├── .swift-version ├── .travis.yml ├── Example ├── Podfile ├── SwiftScraperExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── SwiftScraperExample │ ├── AdvancedTutorialViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── GoogleSearch.js │ ├── Info.plist │ └── TutorialViewController.swift ├── LICENSE ├── Podfile ├── Podfile.lock ├── Pods ├── Manifest.lock ├── Observable-Swift │ ├── LICENSE │ ├── Observable-Swift │ │ ├── Event.swift │ │ ├── EventReference.swift │ │ ├── EventSubscription.swift │ │ ├── Observable.swift │ │ ├── ObservableChainingProxy.swift │ │ ├── ObservableProxy.swift │ │ ├── ObservableReference.swift │ │ ├── OwningEventReference.swift │ │ ├── Protocols.swift │ │ └── TupleObservable.swift │ └── README.md ├── Pods.xcodeproj │ └── project.pbxproj └── Target Support Files │ ├── Observable-Swift │ ├── Info.plist │ ├── Observable-Swift-dummy.m │ ├── Observable-Swift-prefix.pch │ ├── Observable-Swift-umbrella.h │ ├── Observable-Swift.modulemap │ └── Observable-Swift.xcconfig │ ├── Pods-SwiftScraper │ ├── Info.plist │ ├── Pods-SwiftScraper-acknowledgements.markdown │ ├── Pods-SwiftScraper-acknowledgements.plist │ ├── Pods-SwiftScraper-dummy.m │ ├── Pods-SwiftScraper-resources.sh │ ├── Pods-SwiftScraper-umbrella.h │ ├── Pods-SwiftScraper.debug.xcconfig │ ├── Pods-SwiftScraper.modulemap │ └── Pods-SwiftScraper.release.xcconfig │ └── Pods-SwiftScraperTests │ ├── Info.plist │ ├── Pods-SwiftScraperTests-acknowledgements.markdown │ ├── Pods-SwiftScraperTests-acknowledgements.plist │ ├── Pods-SwiftScraperTests-dummy.m │ ├── Pods-SwiftScraperTests-frameworks.sh │ ├── Pods-SwiftScraperTests-resources.sh │ ├── Pods-SwiftScraperTests-umbrella.h │ ├── Pods-SwiftScraperTests.debug.xcconfig │ ├── Pods-SwiftScraperTests.modulemap │ └── Pods-SwiftScraperTests.release.xcconfig ├── README.md ├── Resources └── SwiftScraper.js ├── Sources ├── Browser.swift ├── Info.plist ├── JSON.swift ├── JavaScriptGenerator.swift ├── Result.swift ├── StepRunner.swift ├── Steps │ ├── AsyncScriptStep.swift │ ├── NavigableStep.swift │ ├── OpenPageStep.swift │ ├── PageChangeStep.swift │ ├── ProcessStep.swift │ ├── ScriptStep.swift │ ├── Step.swift │ ├── StepCompletionCallback.swift │ ├── StepFlowResult.swift │ ├── WaitForConditionStep.swift │ └── WaitStep.swift ├── SwiftScraper.h └── SwiftScraperError.swift ├── SwiftScraper.podspec ├── SwiftScraper.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── SwiftScraper.xcscheme ├── SwiftScraper.xcworkspace └── contents.xcworkspacedata └── Tests ├── Info.plist ├── JavaScriptGeneratorTests.swift ├── Pages ├── page1.html ├── page2.html └── waitTest.html ├── StepRunnerTests.js └── StepRunnerTests.swift /.swift-version: -------------------------------------------------------------------------------- 1 | 3.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9.2 3 | 4 | script: 5 | - set -o pipefail 6 | - xcodebuild -version 7 | - xcodebuild -showsdks 8 | - xcodebuild clean test -destination "platform=iOS Simulator,OS=11.2,name=iPhone 8" -workspace SwiftScraper.xcworkspace -scheme SwiftScraper CODE_SIGNING_REQUIRED=NO | xcpretty 9 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | use_frameworks! 3 | 4 | target "SwiftScraperExample" do 5 | pod "SwiftScraper", path: "../" 6 | end 7 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2747BCB1DE0CEFB76639BC91 /* Pods_SwiftScraperExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7683866078428A4013199C1 /* Pods_SwiftScraperExample.framework */; }; 11 | 5B276B4B1ECDD51600ED5D01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B276B4A1ECDD51600ED5D01 /* AppDelegate.swift */; }; 12 | 5B276B4D1ECDD51600ED5D01 /* TutorialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B276B4C1ECDD51600ED5D01 /* TutorialViewController.swift */; }; 13 | 5B276B501ECDD51600ED5D01 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5B276B4E1ECDD51600ED5D01 /* Main.storyboard */; }; 14 | 5B276B521ECDD51600ED5D01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5B276B511ECDD51600ED5D01 /* Assets.xcassets */; }; 15 | 5B276B551ECDD51600ED5D01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5B276B531ECDD51600ED5D01 /* LaunchScreen.storyboard */; }; 16 | 5B276B5D1ECDD81B00ED5D01 /* GoogleSearch.js in Resources */ = {isa = PBXBuildFile; fileRef = 5B276B5C1ECDD81B00ED5D01 /* GoogleSearch.js */; }; 17 | 5B276B5F1ECDD96400ED5D01 /* AdvancedTutorialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B276B5E1ECDD96400ED5D01 /* AdvancedTutorialViewController.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 5B276B471ECDD51600ED5D01 /* SwiftScraperExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftScraperExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 5B276B4A1ECDD51600ED5D01 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | 5B276B4C1ECDD51600ED5D01 /* TutorialViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialViewController.swift; sourceTree = ""; }; 24 | 5B276B4F1ECDD51600ED5D01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 25 | 5B276B511ECDD51600ED5D01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 5B276B541ECDD51600ED5D01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | 5B276B561ECDD51600ED5D01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | 5B276B5C1ECDD81B00ED5D01 /* GoogleSearch.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = GoogleSearch.js; sourceTree = ""; }; 29 | 5B276B5E1ECDD96400ED5D01 /* AdvancedTutorialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvancedTutorialViewController.swift; sourceTree = ""; }; 30 | D661B490B0ADEF3BA9307A6B /* Pods-SwiftScraperExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftScraperExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftScraperExample/Pods-SwiftScraperExample.debug.xcconfig"; sourceTree = ""; }; 31 | F7683866078428A4013199C1 /* Pods_SwiftScraperExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftScraperExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | FBBA70EDE7E3754AA3B5FDE7 /* Pods-SwiftScraperExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftScraperExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftScraperExample/Pods-SwiftScraperExample.release.xcconfig"; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 5B276B441ECDD51600ED5D01 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | 2747BCB1DE0CEFB76639BC91 /* Pods_SwiftScraperExample.framework in Frameworks */, 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 5B276B3E1ECDD51600ED5D01 = { 48 | isa = PBXGroup; 49 | children = ( 50 | 5B276B491ECDD51600ED5D01 /* SwiftScraperExample */, 51 | 5B276B481ECDD51600ED5D01 /* Products */, 52 | ABF4375E368477C2AAE31280 /* Pods */, 53 | 7DE8D30F70BB9FB934CAD82C /* Frameworks */, 54 | ); 55 | sourceTree = ""; 56 | }; 57 | 5B276B481ECDD51600ED5D01 /* Products */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 5B276B471ECDD51600ED5D01 /* SwiftScraperExample.app */, 61 | ); 62 | name = Products; 63 | sourceTree = ""; 64 | }; 65 | 5B276B491ECDD51600ED5D01 /* SwiftScraperExample */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 5B276B4A1ECDD51600ED5D01 /* AppDelegate.swift */, 69 | 5B276B511ECDD51600ED5D01 /* Assets.xcassets */, 70 | 5B276B5C1ECDD81B00ED5D01 /* GoogleSearch.js */, 71 | 5B276B561ECDD51600ED5D01 /* Info.plist */, 72 | 5B276B531ECDD51600ED5D01 /* LaunchScreen.storyboard */, 73 | 5B276B4E1ECDD51600ED5D01 /* Main.storyboard */, 74 | 5B276B4C1ECDD51600ED5D01 /* TutorialViewController.swift */, 75 | 5B276B5E1ECDD96400ED5D01 /* AdvancedTutorialViewController.swift */, 76 | ); 77 | path = SwiftScraperExample; 78 | sourceTree = ""; 79 | }; 80 | 7DE8D30F70BB9FB934CAD82C /* Frameworks */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | F7683866078428A4013199C1 /* Pods_SwiftScraperExample.framework */, 84 | ); 85 | name = Frameworks; 86 | sourceTree = ""; 87 | }; 88 | ABF4375E368477C2AAE31280 /* Pods */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | D661B490B0ADEF3BA9307A6B /* Pods-SwiftScraperExample.debug.xcconfig */, 92 | FBBA70EDE7E3754AA3B5FDE7 /* Pods-SwiftScraperExample.release.xcconfig */, 93 | ); 94 | name = Pods; 95 | sourceTree = ""; 96 | }; 97 | /* End PBXGroup section */ 98 | 99 | /* Begin PBXNativeTarget section */ 100 | 5B276B461ECDD51600ED5D01 /* SwiftScraperExample */ = { 101 | isa = PBXNativeTarget; 102 | buildConfigurationList = 5B276B591ECDD51600ED5D01 /* Build configuration list for PBXNativeTarget "SwiftScraperExample" */; 103 | buildPhases = ( 104 | 511E0A2FF4A41AA1B071F8D1 /* [CP] Check Pods Manifest.lock */, 105 | 5B276B431ECDD51600ED5D01 /* Sources */, 106 | 5B276B441ECDD51600ED5D01 /* Frameworks */, 107 | 5B276B451ECDD51600ED5D01 /* Resources */, 108 | 897F49842C0EB908D305A2F7 /* [CP] Embed Pods Frameworks */, 109 | B8BA8F0D7ED2C914FF07350D /* [CP] Copy Pods Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = SwiftScraperExample; 116 | productName = SwiftScraperExample; 117 | productReference = 5B276B471ECDD51600ED5D01 /* SwiftScraperExample.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | 5B276B3F1ECDD51600ED5D01 /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastSwiftUpdateCheck = 0830; 127 | LastUpgradeCheck = 0830; 128 | ORGANIZATIONNAME = "Ken Ko"; 129 | TargetAttributes = { 130 | 5B276B461ECDD51600ED5D01 = { 131 | CreatedOnToolsVersion = 8.3.2; 132 | ProvisioningStyle = Automatic; 133 | }; 134 | }; 135 | }; 136 | buildConfigurationList = 5B276B421ECDD51600ED5D01 /* Build configuration list for PBXProject "SwiftScraperExample" */; 137 | compatibilityVersion = "Xcode 3.2"; 138 | developmentRegion = English; 139 | hasScannedForEncodings = 0; 140 | knownRegions = ( 141 | en, 142 | Base, 143 | ); 144 | mainGroup = 5B276B3E1ECDD51600ED5D01; 145 | productRefGroup = 5B276B481ECDD51600ED5D01 /* Products */; 146 | projectDirPath = ""; 147 | projectRoot = ""; 148 | targets = ( 149 | 5B276B461ECDD51600ED5D01 /* SwiftScraperExample */, 150 | ); 151 | }; 152 | /* End PBXProject section */ 153 | 154 | /* Begin PBXResourcesBuildPhase section */ 155 | 5B276B451ECDD51600ED5D01 /* Resources */ = { 156 | isa = PBXResourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | 5B276B551ECDD51600ED5D01 /* LaunchScreen.storyboard in Resources */, 160 | 5B276B5D1ECDD81B00ED5D01 /* GoogleSearch.js in Resources */, 161 | 5B276B521ECDD51600ED5D01 /* Assets.xcassets in Resources */, 162 | 5B276B501ECDD51600ED5D01 /* Main.storyboard in Resources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXResourcesBuildPhase section */ 167 | 168 | /* Begin PBXShellScriptBuildPhase section */ 169 | 511E0A2FF4A41AA1B071F8D1 /* [CP] Check Pods Manifest.lock */ = { 170 | isa = PBXShellScriptBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | ); 174 | inputPaths = ( 175 | ); 176 | name = "[CP] Check Pods Manifest.lock"; 177 | outputPaths = ( 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | shellPath = /bin/sh; 181 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 182 | showEnvVarsInLog = 0; 183 | }; 184 | 897F49842C0EB908D305A2F7 /* [CP] Embed Pods Frameworks */ = { 185 | isa = PBXShellScriptBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | ); 189 | inputPaths = ( 190 | ); 191 | name = "[CP] Embed Pods Frameworks"; 192 | outputPaths = ( 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | shellPath = /bin/sh; 196 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftScraperExample/Pods-SwiftScraperExample-frameworks.sh\"\n"; 197 | showEnvVarsInLog = 0; 198 | }; 199 | B8BA8F0D7ED2C914FF07350D /* [CP] Copy Pods Resources */ = { 200 | isa = PBXShellScriptBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | ); 204 | inputPaths = ( 205 | ); 206 | name = "[CP] Copy Pods Resources"; 207 | outputPaths = ( 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | shellPath = /bin/sh; 211 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftScraperExample/Pods-SwiftScraperExample-resources.sh\"\n"; 212 | showEnvVarsInLog = 0; 213 | }; 214 | /* End PBXShellScriptBuildPhase section */ 215 | 216 | /* Begin PBXSourcesBuildPhase section */ 217 | 5B276B431ECDD51600ED5D01 /* Sources */ = { 218 | isa = PBXSourcesBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | 5B276B5F1ECDD96400ED5D01 /* AdvancedTutorialViewController.swift in Sources */, 222 | 5B276B4D1ECDD51600ED5D01 /* TutorialViewController.swift in Sources */, 223 | 5B276B4B1ECDD51600ED5D01 /* AppDelegate.swift in Sources */, 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | /* End PBXSourcesBuildPhase section */ 228 | 229 | /* Begin PBXVariantGroup section */ 230 | 5B276B4E1ECDD51600ED5D01 /* Main.storyboard */ = { 231 | isa = PBXVariantGroup; 232 | children = ( 233 | 5B276B4F1ECDD51600ED5D01 /* Base */, 234 | ); 235 | name = Main.storyboard; 236 | sourceTree = ""; 237 | }; 238 | 5B276B531ECDD51600ED5D01 /* LaunchScreen.storyboard */ = { 239 | isa = PBXVariantGroup; 240 | children = ( 241 | 5B276B541ECDD51600ED5D01 /* Base */, 242 | ); 243 | name = LaunchScreen.storyboard; 244 | sourceTree = ""; 245 | }; 246 | /* End PBXVariantGroup section */ 247 | 248 | /* Begin XCBuildConfiguration section */ 249 | 5B276B571ECDD51600ED5D01 /* Debug */ = { 250 | isa = XCBuildConfiguration; 251 | buildSettings = { 252 | ALWAYS_SEARCH_USER_PATHS = NO; 253 | CLANG_ANALYZER_NONNULL = YES; 254 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 255 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 256 | CLANG_CXX_LIBRARY = "libc++"; 257 | CLANG_ENABLE_MODULES = YES; 258 | CLANG_ENABLE_OBJC_ARC = YES; 259 | CLANG_WARN_BOOL_CONVERSION = YES; 260 | CLANG_WARN_CONSTANT_CONVERSION = YES; 261 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 262 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 263 | CLANG_WARN_EMPTY_BODY = YES; 264 | CLANG_WARN_ENUM_CONVERSION = YES; 265 | CLANG_WARN_INFINITE_RECURSION = YES; 266 | CLANG_WARN_INT_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 272 | COPY_PHASE_STRIP = NO; 273 | DEBUG_INFORMATION_FORMAT = dwarf; 274 | ENABLE_STRICT_OBJC_MSGSEND = YES; 275 | ENABLE_TESTABILITY = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu99; 277 | GCC_DYNAMIC_NO_PIC = NO; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_OPTIMIZATION_LEVEL = 0; 280 | GCC_PREPROCESSOR_DEFINITIONS = ( 281 | "DEBUG=1", 282 | "$(inherited)", 283 | ); 284 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 285 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 286 | GCC_WARN_UNDECLARED_SELECTOR = YES; 287 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 288 | GCC_WARN_UNUSED_FUNCTION = YES; 289 | GCC_WARN_UNUSED_VARIABLE = YES; 290 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 291 | MTL_ENABLE_DEBUG_INFO = YES; 292 | ONLY_ACTIVE_ARCH = YES; 293 | SDKROOT = iphoneos; 294 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 295 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 296 | }; 297 | name = Debug; 298 | }; 299 | 5B276B581ECDD51600ED5D01 /* Release */ = { 300 | isa = XCBuildConfiguration; 301 | buildSettings = { 302 | ALWAYS_SEARCH_USER_PATHS = NO; 303 | CLANG_ANALYZER_NONNULL = YES; 304 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 306 | CLANG_CXX_LIBRARY = "libc++"; 307 | CLANG_ENABLE_MODULES = YES; 308 | CLANG_ENABLE_OBJC_ARC = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_CONSTANT_CONVERSION = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 313 | CLANG_WARN_EMPTY_BODY = YES; 314 | CLANG_WARN_ENUM_CONVERSION = YES; 315 | CLANG_WARN_INFINITE_RECURSION = YES; 316 | CLANG_WARN_INT_CONVERSION = YES; 317 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 318 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 319 | CLANG_WARN_UNREACHABLE_CODE = YES; 320 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 321 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 322 | COPY_PHASE_STRIP = NO; 323 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 324 | ENABLE_NS_ASSERTIONS = NO; 325 | ENABLE_STRICT_OBJC_MSGSEND = YES; 326 | GCC_C_LANGUAGE_STANDARD = gnu99; 327 | GCC_NO_COMMON_BLOCKS = YES; 328 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 329 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 330 | GCC_WARN_UNDECLARED_SELECTOR = YES; 331 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 332 | GCC_WARN_UNUSED_FUNCTION = YES; 333 | GCC_WARN_UNUSED_VARIABLE = YES; 334 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 335 | MTL_ENABLE_DEBUG_INFO = NO; 336 | SDKROOT = iphoneos; 337 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 338 | VALIDATE_PRODUCT = YES; 339 | }; 340 | name = Release; 341 | }; 342 | 5B276B5A1ECDD51600ED5D01 /* Debug */ = { 343 | isa = XCBuildConfiguration; 344 | baseConfigurationReference = D661B490B0ADEF3BA9307A6B /* Pods-SwiftScraperExample.debug.xcconfig */; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | INFOPLIST_FILE = SwiftScraperExample/Info.plist; 348 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 349 | PRODUCT_BUNDLE_IDENTIFIER = com.cweatureapps.SwiftScraperExample; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | SWIFT_VERSION = 3.0; 352 | }; 353 | name = Debug; 354 | }; 355 | 5B276B5B1ECDD51600ED5D01 /* Release */ = { 356 | isa = XCBuildConfiguration; 357 | baseConfigurationReference = FBBA70EDE7E3754AA3B5FDE7 /* Pods-SwiftScraperExample.release.xcconfig */; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 360 | INFOPLIST_FILE = SwiftScraperExample/Info.plist; 361 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 362 | PRODUCT_BUNDLE_IDENTIFIER = com.cweatureapps.SwiftScraperExample; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SWIFT_VERSION = 3.0; 365 | }; 366 | name = Release; 367 | }; 368 | /* End XCBuildConfiguration section */ 369 | 370 | /* Begin XCConfigurationList section */ 371 | 5B276B421ECDD51600ED5D01 /* Build configuration list for PBXProject "SwiftScraperExample" */ = { 372 | isa = XCConfigurationList; 373 | buildConfigurations = ( 374 | 5B276B571ECDD51600ED5D01 /* Debug */, 375 | 5B276B581ECDD51600ED5D01 /* Release */, 376 | ); 377 | defaultConfigurationIsVisible = 0; 378 | defaultConfigurationName = Release; 379 | }; 380 | 5B276B591ECDD51600ED5D01 /* Build configuration list for PBXNativeTarget "SwiftScraperExample" */ = { 381 | isa = XCConfigurationList; 382 | buildConfigurations = ( 383 | 5B276B5A1ECDD51600ED5D01 /* Debug */, 384 | 5B276B5B1ECDD51600ED5D01 /* Release */, 385 | ); 386 | defaultConfigurationIsVisible = 0; 387 | defaultConfigurationName = Release; 388 | }; 389 | /* End XCConfigurationList section */ 390 | }; 391 | rootObject = 5B276B3F1ECDD51600ED5D01 /* Project object */; 392 | } 393 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample/AdvancedTutorialViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedTutorialViewController.swift 3 | // SwiftScraperExample 4 | // 5 | // Created by Ken Ko on 18/5/17. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import SwiftScraper 12 | 13 | /// This example does a google image search, and then keeps scrolling down to the bottom, 14 | /// until there are no more new images loaded. 15 | class AdvancedTutorialViewController: UIViewController { 16 | 17 | var stepRunner: StepRunner! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | let step1 = OpenPageStep(path: "https://www.google.com.au/search?tbm=isch") 23 | 24 | let step2 = PageChangeStep( 25 | functionName: "performSearch", 26 | params: "ankylosaurus") 27 | 28 | let step3 = AsyncScriptStep(functionName: "scrollAndCountImages") { response, model in 29 | if let json = response as? JSON { 30 | if let first = json["first"], let second = json["second"] { 31 | print("first: ", first, "second: ", second) 32 | 33 | // Save the data to the model dictionary 34 | model["first"] = first 35 | model["second"] = second 36 | } 37 | } 38 | return .proceed 39 | } 40 | 41 | // Keep looping back to `step3` until the before count and after count are the same 42 | let conditionStep = ProcessStep { model in 43 | if let first = model["first"] as? Int, 44 | let second = model["second"] as? Int, 45 | first == second { 46 | return .proceed 47 | } else { 48 | return .jumpToStep(2) // This is a zero-based index, i.e. step3 49 | } 50 | } 51 | 52 | stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1, step2, step3, conditionStep]) 53 | stepRunner.insertWebViewIntoView(parent: view) 54 | stepRunner.state.afterChange.add { change in 55 | print("-----", change.newValue, "-----") 56 | switch change.newValue { 57 | case .inProgress(let index): 58 | print("About to run step at index", index) 59 | case .failure(let error): 60 | print("Failed: ", error.localizedDescription) 61 | case .success: 62 | print("Finished successfully") 63 | default: 64 | break 65 | } 66 | } 67 | stepRunner.run() 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftScraperExample 4 | // 5 | // Created by Ken Ko on 18/5/17. 6 | // Copyright © 2017 Ken Ko. 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: [UIApplicationLaunchOptionsKey: Any]?) -> 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 invalidate graphics rendering callbacks. 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 active 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/SwiftScraperExample/Assets.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 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Example/SwiftScraperExample/Base.lproj/LaunchScreen.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 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample/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 | 31 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample/GoogleSearch.js: -------------------------------------------------------------------------------- 1 | var GoogleSearch = (function () { 2 | function assertGoogleTitle() { 3 | return document.title == "Google"; 4 | } 5 | function performSearch(searchText) { 6 | document.querySelector('input[type="text"], input[type="Search"]').value = searchText; 7 | document.forms[0].submit(); 8 | } 9 | function assertSearchResultTitle() { 10 | return document.title == "SwiftScraper iOS - Google Search"; 11 | } 12 | function getSearchResults() { 13 | var headings = document.querySelectorAll('h3.r'); 14 | return Array.prototype.slice.call(headings).map(function (h3) { 15 | return { 'text': h3.innerText, 'href': h3.childNodes[0].href }; 16 | }); 17 | } 18 | function scrollAndCountImages() { 19 | var firstCount = document.querySelectorAll('img').length; 20 | window.scrollTo(0, document.body.scrollHeight); 21 | setTimeout(function () { 22 | var secondCount = document.querySelectorAll('img').length; 23 | SwiftScraper.postMessage({ 'first': firstCount, 'second': secondCount }); 24 | }, 2000); 25 | } 26 | return { 27 | assertGoogleTitle: assertGoogleTitle, 28 | performSearch: performSearch, 29 | assertSearchResultTitle: assertSearchResultTitle, 30 | getSearchResults: getSearchResults, 31 | scrollAndCountImages: scrollAndCountImages 32 | }; 33 | })() 34 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | NSAppTransportSecurity 38 | 39 | NSAllowsArbitraryLoads 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/SwiftScraperExample/TutorialViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TutorialViewController.swift 3 | // SwiftScraperExample 4 | // 5 | // Created by Ken Ko on 18/5/17. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import SwiftScraper 12 | 13 | /// This example performs a search on Google, and prints the results. 14 | class TutorialViewController: UIViewController { 15 | var stepRunner: StepRunner! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | let step1 = OpenPageStep( 21 | path: "https://www.google.com", 22 | assertionName: "assertGoogleTitle") 23 | 24 | let step2 = PageChangeStep( 25 | functionName: "performSearch", 26 | params: "SwiftScraper iOS", 27 | assertionName: "assertSearchResultTitle") 28 | 29 | let step3 = ScriptStep(functionName: "getSearchResults") { response, _ in 30 | if let responseArray = response as? [JSON] { 31 | responseArray.forEach { json in 32 | if let text = json["text"], let href = json["href"] { 33 | print(text, "(", href, ")") 34 | } 35 | } 36 | } 37 | return .proceed 38 | } 39 | 40 | stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1, step2, step3]) 41 | stepRunner.insertWebViewIntoView(parent: view) 42 | stepRunner.state.afterChange.add { change in 43 | print("-----", change.newValue, "-----") 44 | switch change.newValue { 45 | case .inProgress(let index): 46 | print("About to run step at index", index) 47 | case .failure(let error): 48 | print("Failed: ", error.localizedDescription) 49 | case .success: 50 | print("Finished successfully") 51 | default: 52 | break 53 | } 54 | } 55 | stepRunner.run() 56 | } 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Kenneth Ko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, "8.0" 2 | 3 | use_frameworks! 4 | inhibit_all_warnings! 5 | 6 | target "SwiftScraper" do 7 | pod 'Observable-Swift', '~> 0.7' 8 | target "SwiftScraperTests" do 9 | inherit! :search_paths 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Observable-Swift (0.7.0) 3 | 4 | DEPENDENCIES: 5 | - Observable-Swift (~> 0.7) 6 | 7 | SPEC CHECKSUMS: 8 | Observable-Swift: 8725f501976e49db368a2cd37277d17a9a51a8aa 9 | 10 | PODFILE CHECKSUM: 0a1b6b4e00ba24a0cb1f9c35f5980cf20e298b5b 11 | 12 | COCOAPODS: 1.1.1 13 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Observable-Swift (0.7.0) 3 | 4 | DEPENDENCIES: 5 | - Observable-Swift (~> 0.7) 6 | 7 | SPEC CHECKSUMS: 8 | Observable-Swift: 8725f501976e49db368a2cd37277d17a9a51a8aa 9 | 10 | PODFILE CHECKSUM: 0a1b6b4e00ba24a0cb1f9c35f5980cf20e298b5b 11 | 12 | COCOAPODS: 1.1.1 13 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Leszek Ślażyński 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 21/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | // Events are implemented as structs, what has both advantages and disadvantages 10 | // Notably they are copied when inside other value types, and mutated on add/remove/notify 11 | // If you require a reference type for Event, use EventReference instead 12 | 13 | /// A struct representing a collection of subscriptions with means to add, remove and notify them. 14 | public struct Event: UnownableEvent { 15 | public typealias ValueType = T 16 | public typealias SubscriptionType = EventSubscription 17 | public typealias HandlerType = SubscriptionType.HandlerType 18 | 19 | public private(set) var subscriptions = [SubscriptionType]() 20 | 21 | public init() { } 22 | 23 | public mutating func notify(_ value: T) { 24 | subscriptions = subscriptions.filter { $0.valid() } 25 | for subscription in subscriptions { 26 | subscription.handler(value) 27 | } 28 | } 29 | 30 | @discardableResult 31 | public mutating func add(_ subscription: SubscriptionType) -> SubscriptionType { 32 | subscriptions.append(subscription) 33 | return subscription 34 | } 35 | 36 | @discardableResult 37 | public mutating func add(_ handler: @escaping HandlerType) -> SubscriptionType { 38 | return add(SubscriptionType(owner: nil, handler: handler)) 39 | } 40 | 41 | public mutating func remove(_ subscription: SubscriptionType) { 42 | var newsubscriptions = [SubscriptionType]() 43 | var first = true 44 | for existing in subscriptions { 45 | if first && existing === subscription { 46 | first = false 47 | } else { 48 | newsubscriptions.append(existing) 49 | } 50 | } 51 | subscriptions = newsubscriptions 52 | } 53 | 54 | public mutating func removeAll() { 55 | subscriptions.removeAll() 56 | } 57 | 58 | @discardableResult 59 | public mutating func add(owner: AnyObject, _ handler: @escaping HandlerType) -> SubscriptionType { 60 | return add(SubscriptionType(owner: owner, handler: handler)) 61 | } 62 | 63 | public mutating func unshare() { 64 | // _subscriptions.unshare() 65 | } 66 | 67 | } 68 | 69 | @discardableResult 70 | public func += (event: inout T, handler: @escaping (T.ValueType) -> ()) -> EventSubscription { 71 | return event.add(handler) 72 | } 73 | 74 | @discardableResult 75 | public func += (event: T, handler: @escaping (T.ValueType) -> ()) -> EventSubscription { 76 | var e = event 77 | return e.add(handler) 78 | } 79 | 80 | public func -= (event: inout T, subscription: EventSubscription) { 81 | return event.remove(subscription) 82 | } 83 | 84 | public func -= (event: T, subscription: EventSubscription) { 85 | var e = event 86 | return e.remove(subscription) 87 | } 88 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/EventReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventReference.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 23/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | /// A class enclosing an Event struct. Thus exposing it as a reference type. 10 | open class EventReference: OwnableEvent { 11 | public typealias ValueType = T 12 | public typealias SubscriptionType = EventSubscription 13 | public typealias HandlerType = EventSubscription.HandlerType 14 | 15 | public private(set) var event: Event 16 | 17 | open func notify(_ value: T) { 18 | event.notify(value) 19 | } 20 | 21 | @discardableResult 22 | open func add(_ subscription: SubscriptionType) -> SubscriptionType { 23 | return event.add(subscription) 24 | } 25 | 26 | @discardableResult 27 | open func add(_ handler: @escaping (T) -> ()) -> EventSubscription { 28 | return event.add(handler) 29 | } 30 | 31 | open func remove(_ subscription: SubscriptionType) { 32 | return event.remove(subscription) 33 | } 34 | 35 | open func removeAll() { 36 | event.removeAll() 37 | } 38 | 39 | @discardableResult 40 | open func add(owner: AnyObject, _ handler: @escaping HandlerType) -> SubscriptionType { 41 | return event.add(owner: owner, handler) 42 | } 43 | 44 | public convenience init() { 45 | self.init(event: Event()) 46 | } 47 | 48 | public init(event: Event) { 49 | self.event = event 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/EventSubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventSubscription.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 21/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | // Implemented as a class, so it can be compared using === and !==. 10 | 11 | // There is no way for event to get notified when the owner was deallocated, 12 | // therefore it will be invalidated only upon next attempt to trigger. 13 | 14 | // Event subscriptions are neither freed nor removed from events upon invalidation. 15 | // Events remove invalidated subscriptions themselves when firing. 16 | 17 | // Invalidation immediately frees handler and owned objects. 18 | 19 | /// A class representing a subscription for `Event`. 20 | public class EventSubscription { 21 | 22 | public typealias HandlerType = (T) -> () 23 | 24 | private var _valid: () -> Bool 25 | 26 | /// Handler to be caled when value changes. 27 | public private(set) var handler: HandlerType 28 | 29 | /// array of owned objects 30 | private var _owned = [AnyObject]() 31 | 32 | /// When invalid subscription is to be notified, it is removed instead. 33 | public func valid() -> Bool { 34 | if !_valid() { 35 | invalidate() 36 | return false 37 | } else { 38 | return true 39 | } 40 | } 41 | 42 | /// Marks the event for removal, frees the handler and owned objects 43 | public func invalidate() { 44 | _valid = { false } 45 | handler = { _ in () } 46 | _owned = [] 47 | } 48 | 49 | /// Init with a handler and an optional owner. 50 | /// If owner is present, valid() is tied to its lifetime. 51 | public init(owner o: AnyObject?, handler h: @escaping HandlerType) { 52 | if o == nil { 53 | _valid = { true } 54 | } else { 55 | _valid = { [weak o] in o != nil } 56 | } 57 | handler = h 58 | } 59 | 60 | /// Add an object to be owned while the event is not invalidated 61 | public func addOwnedObject(_ o: AnyObject) { 62 | _owned.append(o) 63 | } 64 | 65 | /// Remove object from owned objects 66 | public func removeOwnedObject(_ o: AnyObject) { 67 | _owned = _owned.filter{ $0 !== o } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/Observable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observable.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 20/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | /// A struct representing information associated with value change event. 10 | public struct ValueChange { 11 | public let oldValue: T 12 | public let newValue: T 13 | public init(_ o: T, _ n: T) { 14 | oldValue = o 15 | newValue = n 16 | } 17 | } 18 | 19 | // Implemented as a struct in order to have desired value and mutability sementics. 20 | 21 | /// A struct representing an observable value. 22 | public struct Observable: UnownableObservable { 23 | 24 | public typealias ValueType = T 25 | 26 | public private(set) var beforeChange = EventReference>() 27 | public private(set) var afterChange = EventReference>() 28 | 29 | public var value : T { 30 | willSet { beforeChange.notify(ValueChange(value, newValue)) } 31 | didSet { afterChange.notify(ValueChange(oldValue, value)) } 32 | } 33 | 34 | public mutating func unshare(removeSubscriptions: Bool) { 35 | if removeSubscriptions { 36 | beforeChange = EventReference>() 37 | afterChange = EventReference>() 38 | } else { 39 | var beforeEvent = beforeChange.event 40 | beforeEvent.unshare() 41 | beforeChange = EventReference>(event: beforeEvent) 42 | var afterEvent = afterChange.event 43 | afterEvent.unshare() 44 | afterChange = EventReference>(event: afterEvent) 45 | } 46 | } 47 | 48 | public init(_ v : T) { 49 | value = v 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/ObservableChainingProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Chaining.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 23/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | public class ObservableChainingProxy: OwnableObservable { 10 | 11 | public typealias ValueType = O2.ValueType? 12 | 13 | public var value: ValueType { return nil } 14 | 15 | private weak var _beforeChange: EventReference>? = nil 16 | private weak var _afterChange: EventReference>? = nil 17 | 18 | public var beforeChange: EventReference> { 19 | if let event = _beforeChange { 20 | return event 21 | } else { 22 | let event = OwningEventReference>() 23 | event.owned = self 24 | _beforeChange = event 25 | return event 26 | } 27 | } 28 | 29 | public var afterChange: EventReference> { 30 | if let event = _afterChange { 31 | return event 32 | } else { 33 | let event = OwningEventReference>() 34 | event.owned = self 35 | _afterChange = event 36 | return event 37 | } 38 | } 39 | 40 | private let base: O1 41 | private let path: (O1.ValueType) -> O2? 42 | 43 | private func targetChangeToValueChange(_ vc: ValueChange) -> ValueChange { 44 | let oldValue = Optional.some(vc.oldValue) 45 | let newValue = Optional.some(vc.newValue) 46 | return ValueChange(oldValue, newValue) 47 | } 48 | 49 | private func objectChangeToValueChange(_ oc: ValueChange) -> ValueChange { 50 | let oldValue = path(oc.oldValue)?.value 51 | let newValue = path(oc.newValue)?.value 52 | return ValueChange(oldValue, newValue) 53 | } 54 | 55 | init(base: O1, path: @escaping (O1.ValueType) -> O2?) { 56 | self.base = base 57 | self.path = path 58 | 59 | let beforeSubscription = EventSubscription(owner: self) { [weak self] in 60 | self!.beforeChange.notify(self!.targetChangeToValueChange($0)) 61 | } 62 | 63 | let afterSubscription = EventSubscription(owner: self) { [weak self] in 64 | self!.afterChange.notify(self!.targetChangeToValueChange($0)) 65 | } 66 | 67 | base.beforeChange.add(owner: self) { [weak self] oc in 68 | let oldTarget = path(oc.oldValue) 69 | oldTarget?.beforeChange.remove(beforeSubscription) 70 | oldTarget?.afterChange.remove(afterSubscription) 71 | self!.beforeChange.notify(self!.objectChangeToValueChange(oc)) 72 | } 73 | 74 | base.afterChange.add(owner: self) { [weak self] oc in 75 | self!.afterChange.notify(self!.objectChangeToValueChange(oc)) 76 | let newTarget = path(oc.newValue) 77 | newTarget?.beforeChange.add(beforeSubscription) 78 | newTarget?.afterChange.add(afterSubscription) 79 | } 80 | } 81 | 82 | public func to(path f: @escaping (O2.ValueType) -> O3?) -> ObservableChainingProxy, O3> { 83 | func cascadeNil(_ oOrNil: ValueType) -> O3? { 84 | if let o = oOrNil { 85 | return f(o) 86 | } else { 87 | return nil 88 | } 89 | } 90 | return ObservableChainingProxy, O3>(base: self, path: cascadeNil) 91 | } 92 | 93 | public func to(path f: @escaping (O2.ValueType) -> O3) -> ObservableChainingProxy, O3> { 94 | func cascadeNil(_ oOrNil: ValueType) -> O3? { 95 | if let o = oOrNil { 96 | return f(o) 97 | } else { 98 | return nil 99 | } 100 | } 101 | return ObservableChainingProxy, O3>(base: self, path: cascadeNil) 102 | } 103 | 104 | } 105 | 106 | public struct ObservableChainingBase { 107 | fileprivate let base: O1 108 | public func to(_ path: @escaping (O1.ValueType) -> O2?) -> ObservableChainingProxy { 109 | return ObservableChainingProxy(base: base, path: path) 110 | } 111 | public func to(_ path: @escaping (O1.ValueType) -> O2) -> ObservableChainingProxy { 112 | return ObservableChainingProxy(base: base, path: { .some(path($0)) }) 113 | } 114 | } 115 | 116 | public func chain(_ o: O) -> ObservableChainingBase { 117 | return ObservableChainingBase(base: o) 118 | } 119 | 120 | public func / (o: ObservableChainingProxy, f: @escaping (O2.ValueType) -> O3?) -> ObservableChainingProxy, O3> { 121 | return o.to(path: f) 122 | } 123 | 124 | public func / (o: O1, f: @escaping (O1.ValueType) -> O2?) -> ObservableChainingProxy { 125 | return ObservableChainingProxy(base: o, path: f) 126 | } 127 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/ObservableProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableProxy.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 24/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | // two generic parameters are needed to be able to override `value` in `ObservableReference` 10 | 11 | open class ObservableProxy : OwnableObservable where O.ValueType == T { 12 | 13 | public typealias ValueType = T 14 | 15 | public private(set) var beforeChange = EventReference>() 16 | public private(set) var afterChange = EventReference>() 17 | 18 | // private storage in case subclasses override value with a setter 19 | private var _value: T 20 | 21 | open var value: T { 22 | return _value 23 | } 24 | 25 | public init(_ o: O) { 26 | self._value = o.value 27 | o.beforeChange.add(owner: self) { [weak self] change in 28 | self!.beforeChange.notify(change) 29 | } 30 | o.afterChange.add(owner: self) { [weak self] change in 31 | let nV = change.newValue 32 | self!._value = nV 33 | self!.afterChange.notify(change) 34 | } 35 | } 36 | 37 | } 38 | 39 | public func proxy (_ o: O) -> ObservableProxy { 40 | return ObservableProxy(o) 41 | } 42 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/ObservableReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableReference.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 21/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | public class ObservableReference : ObservableProxy>, WritableObservable { 10 | 11 | public typealias ValueType = T 12 | 13 | private var storage: Observable 14 | 15 | public override var value: T { 16 | get { return storage.value } 17 | set { storage.value = newValue } 18 | } 19 | 20 | public init(_ v : T) { 21 | storage = Observable(v) 22 | super.init(storage) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/OwningEventReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OwningEventReference.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 28/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | /// A subclass of event reference allowing it to own other object[s]. 10 | /// Additionally, the reference makes added events own itself. 11 | /// This retain cycle allows owned objects to live as long as valid subscriptions exist. 12 | public class OwningEventReference: EventReference { 13 | 14 | internal var owned: AnyObject? = nil 15 | 16 | public override func add(_ subscription: SubscriptionType) -> SubscriptionType { 17 | let subscr = super.add(subscription) 18 | if owned != nil { 19 | subscr.addOwnedObject(self) 20 | } 21 | return subscr 22 | } 23 | 24 | public override func add(_ handler: @escaping (T) -> ()) -> EventSubscription { 25 | let subscr = super.add(handler) 26 | if owned != nil { 27 | subscr.addOwnedObject(self) 28 | } 29 | return subscr 30 | } 31 | 32 | public override func remove(_ subscription: SubscriptionType) { 33 | subscription.removeOwnedObject(self) 34 | super.remove(subscription) 35 | } 36 | 37 | public override func removeAll() { 38 | for subscription in event.subscriptions { 39 | subscription.removeOwnedObject(self) 40 | } 41 | super.removeAll() 42 | } 43 | 44 | public override func add(owner: AnyObject, _ handler: @escaping HandlerType) -> SubscriptionType { 45 | let subscr = super.add(owner: owner, handler) 46 | if owned != nil { 47 | subscr.addOwnedObject(self) 48 | } 49 | return subscr 50 | } 51 | 52 | public override init(event: Event) { 53 | super.init(event: event) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/Protocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocols.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 21/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | /// Arbitrary Event. 10 | public protocol AnyEvent { 11 | 12 | associatedtype ValueType 13 | 14 | /// Notify all valid subscriptions of the change. Remove invalid ones. 15 | mutating func notify(_ value: ValueType) 16 | 17 | /// Add an existing subscription. 18 | @discardableResult 19 | mutating func add(_ subscription: EventSubscription) -> EventSubscription 20 | 21 | /// Create, add and return a subscription for given handler. 22 | @discardableResult 23 | mutating func add(_ handler : @escaping (ValueType) -> ()) -> EventSubscription 24 | 25 | /// Remove given subscription, if present. 26 | mutating func remove(_ subscription : EventSubscription) 27 | 28 | /// Remove all subscriptions. 29 | mutating func removeAll() 30 | 31 | /// Create, add and return a subscription with given handler and owner. 32 | @discardableResult 33 | mutating func add(owner : AnyObject, _ handler : @escaping (ValueType) -> ()) -> EventSubscription 34 | 35 | } 36 | 37 | /// Event which is a value type. 38 | public protocol UnownableEvent: AnyEvent { } 39 | 40 | /// Event which is a reference type 41 | public protocol OwnableEvent: AnyEvent { } 42 | 43 | /// Arbitrary observable. 44 | public protocol AnyObservable { 45 | 46 | associatedtype ValueType 47 | 48 | /// Value of the observable. 49 | var value: ValueType { get } 50 | 51 | /// Event fired before value is changed 52 | var beforeChange: EventReference> { get } 53 | 54 | /// Event fired after value is changed 55 | var afterChange: EventReference> { get } 56 | } 57 | 58 | /// Observable which can be written to 59 | public protocol WritableObservable: AnyObservable { 60 | var value: ValueType { get set } 61 | } 62 | 63 | /// Observable which is a value type. Elementary observables are value types. 64 | public protocol UnownableObservable: WritableObservable { 65 | /// Unshares events 66 | mutating func unshare(removeSubscriptions: Bool) 67 | } 68 | 69 | /// Observable which is a reference type. Compound observables are reference types. 70 | public protocol OwnableObservable: AnyObservable { 71 | 72 | } 73 | 74 | // observable <- value 75 | infix operator <- 76 | 77 | // value = observable^ 78 | postfix operator ^ 79 | 80 | // observable ^= value 81 | public func ^= (x: inout T, y: T.ValueType) { 82 | x.value = y 83 | } 84 | 85 | // observable += { valuechange in ... } 86 | @discardableResult 87 | public func += (x: inout T, y: @escaping (ValueChange) -> ()) -> EventSubscription> { 88 | return x.afterChange += y 89 | } 90 | 91 | // observable += { (old, new) in ... } 92 | @discardableResult 93 | public func += (x: inout T, y: @escaping (T.ValueType, T.ValueType) -> ()) -> EventSubscription> { 94 | return x.afterChange += y 95 | } 96 | 97 | // observable += { new in ... } 98 | @discardableResult 99 | public func += (x: inout T, y: @escaping (T.ValueType) -> ()) -> EventSubscription> { 100 | return x.afterChange += y 101 | } 102 | 103 | // observable -= subscription 104 | public func -= (x: inout T, s: EventSubscription>) { 105 | x.afterChange.remove(s) 106 | } 107 | 108 | // event += { (old, new) in ... } 109 | @discardableResult 110 | public func += (event: EventReference>, handler: @escaping (T, T) -> ()) -> EventSubscription> { 111 | return event.add({ handler($0.oldValue, $0.newValue) }) 112 | } 113 | 114 | // event += { new in ... } 115 | @discardableResult 116 | public func += (event: EventReference>, handler: @escaping (T) -> ()) -> EventSubscription> { 117 | return event.add({ handler($0.newValue) }) 118 | } 119 | 120 | // for observable values on variables 121 | public func <- (x: inout T, y: T.ValueType) { 122 | x.value = y 123 | } 124 | 125 | // for observable references on variables or constants 126 | public func <- (x: T, y: T.ValueType) { 127 | var z = x 128 | z.value = y 129 | } 130 | 131 | public postfix func ^ (x: T) -> T.ValueType { 132 | return x.value 133 | } 134 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/Observable-Swift/TupleObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TupleObservable.swift 3 | // Observable-Swift 4 | // 5 | // Created by Leszek Ślażyński on 20/06/14. 6 | // Copyright (c) 2014 Leszek Ślażyński. All rights reserved. 7 | // 8 | 9 | public class PairObservable : OwnableObservable { 10 | 11 | internal typealias T1 = O1.ValueType 12 | internal typealias T2 = O2.ValueType 13 | 14 | public typealias ValueType = (T1, T2) 15 | 16 | public private(set) var beforeChange = EventReference>() 17 | public private(set) var afterChange = EventReference>() 18 | 19 | internal var first : T1 20 | internal var second : T2 21 | 22 | public var value : (T1, T2) { return (first, second) } 23 | 24 | private let _base1 : O1 25 | private let _base2 : O2 26 | 27 | public init (_ o1: O1, _ o2: O2) { 28 | _base1 = o1 29 | _base2 = o2 30 | first = o1.value 31 | second = o2.value 32 | o1.beforeChange.add(owner: self) { [weak self] c1 in 33 | let oldV = (c1.oldValue, self!.second) 34 | let newV = (c1.newValue, self!.second) 35 | let change = ValueChange(oldV, newV) 36 | self!.beforeChange.notify(change) 37 | } 38 | o1.afterChange.add(owner: self) { [weak self] c1 in 39 | let nV1 = c1.newValue 40 | self!.first = nV1 41 | let oldV = (c1.oldValue, self!.second) 42 | let newV = (c1.newValue, self!.second) 43 | let change = ValueChange(oldV, newV) 44 | self!.afterChange.notify(change) 45 | } 46 | o2.beforeChange.add(owner: self) { [weak self] c2 in 47 | let oldV = (self!.first, c2.oldValue) 48 | let newV = (self!.first, c2.newValue) 49 | let change = ValueChange(oldV, newV) 50 | self!.beforeChange.notify(change) 51 | } 52 | o2.afterChange.add(owner: self) { [weak self] c2 in 53 | let nV2 = c2.newValue 54 | self!.second = nV2 55 | let oldV = (self!.first, c2.oldValue) 56 | let newV = (self!.first, c2.newValue) 57 | let change = ValueChange(oldV, newV) 58 | self!.afterChange.notify(change) 59 | } 60 | } 61 | 62 | } 63 | 64 | public func & (x: O1, y: O2) -> PairObservable { 65 | return PairObservable(x, y) 66 | } 67 | -------------------------------------------------------------------------------- /Pods/Observable-Swift/README.md: -------------------------------------------------------------------------------- 1 | # Value Observing and Events for Swift 2 | 3 | Swift lacks the powerful Key Value Observing (KVO) from Objective-C. But thanks to closures, generics and property observers, in some cases it allows for far more elegant observing. You have to be explicit about what can be observed, though. 4 | 5 | ## Overview 6 | 7 | Observable-Swift is a Swift library for value observing (via explicit usage of `Observable`) and subscribable events (also explicit, using `Event`). While it is not exactly "KVO for Swift" (it is explicit, there are no "Keys", ...) it is a catchy name so you can call it that if you want. The library is still under development, just as Swift is. Any contributions, both in terms of suggestions/ideas or actual code are welcome. 8 | 9 | Observable-Swift is brought to you by Leszek Ślażyński (slazyk), you can follow me on [twitter](https://twitter.com/slazyk) and [github](https://github.com/slazyk). 10 | Also check out [SINQ](https://github.com/slazyk/SINQ) my other Swift library that makes working with collections a breeze. 11 | 12 | ### Observables 13 | 14 | Using `Observable` and related classes you can implement wide range of patterns using value observing. Some of the features: 15 | 16 | - observable variables and properties 17 | - chaining of observables (a.k.a. key path observing) 18 | - short readable syntax using `+=`, `-=`, `<-`/`^=`, `^` 19 | - alternative syntax for those who dislike custom operators 20 | - handlers for _before_ or _after_ the change 21 | - handlers for `{ oldValue:, newValue: }` `(oldValue, newValue)` or `(newValue)` 22 | - adding multiple handlers per observable 23 | - removing / invalidating handlers 24 | - handlers tied to observer lifetime 25 | - observable mutations of value types (structs, tuples, ...) 26 | - ~~conversions from observables to underlying type~~ (not available since Swift Beta 6) 27 | - observables combining other observables 28 | - observables as value types or reference types 29 | - ... 30 | 31 | ### Events 32 | 33 | Sometimes, you don’t want to observe for value change, but other significant events. 34 | Under the hood `Observable` uses `beforeChange` and `afterChange` of `EventReference>`. You can, however, use `Event` or `EventReference` directly and implement other events too. 35 | 36 | ## Installation 37 | 38 | You can use either [CocoaPods](https://cocoapods.org/) or [Carthage](https://github.com/Carthage/Carthage) to install Observable-Swift. 39 | 40 | Otherwise, the easiest option to use Observable-Swift in your project is to clone this repo and add Observable-Swift.xcodeproj to your project/workspace and then add Observable.framework to frameworks for your target. 41 | 42 | After that you just `import Observable`. 43 | 44 | ## Examples 45 | `Observable` is a simple `struct` allowing you to have observable variables. 46 | 47 | ```swift 48 | // create a Observable variable 49 | var x = Observable(0) 50 | 51 | // add a handler 52 | x.afterChange += { println("Changed x from \($0) to \($1)") } 53 | // without operators: x.afterChange.add { ... } 54 | 55 | // change the value, prints "Changed x from 0 to 42" 56 | x <- 42 57 | // alternativelyL x ^= 42, without operators: x.value = 42 58 | ``` 59 | 60 | You can, of course, have observable properties in a `class` or a `struct`: 61 | 62 | ```swift 63 | struct Person { 64 | let first: String 65 | var last: Observable 66 | 67 | init(first: String, last: String) { 68 | self.first = first 69 | self.last = Observable(last) 70 | } 71 | } 72 | 73 | var ramsay = Person(first: "Ramsay", last: "Snow") 74 | ramsay.last.afterChange += { println("Ramsay \($0) is now Ramsay \($1)") } 75 | ramsay.last <- "Bolton" 76 | ``` 77 | Up to Swift Beta 5 you could implicitly convert `Observable` to `T`, and use it in places where `T` is expected. Unfortunately Beta 6 forbids defining implicit conversions: 78 | ```swift 79 | let x = Observable(20) 80 | // You can use the value property ... 81 | let y1 = x.value + 22 82 | // ... or a postfix operator ... 83 | let y2 = x^ + 22 84 | /// ... which has the advantage of easy chaining 85 | let y3 = obj.property^.whatever^.sthElse^ 86 | /// ... you can also use ^= instead of <- for consistency with the postfix ^ 87 | ``` 88 | 89 | For value types (such as `structs` or `tuples`) you can also observe their mutations: 90 | *Since `Observable` is a `struct`, ramsay in example above gets mutated too. This means, you could observe ramsay as well.* 91 | 92 | ```swift 93 | struct Person { 94 | let first: String 95 | var last: String 96 | var full: String { get { return "\(first) \(last)" } } 97 | } 98 | 99 | var ramsay = Observable(Person(first: "Ramsay", last: "Snow")) 100 | // x += { ... } is the same as x.afterChange += { ... } 101 | ramsay += { println("\($0.full) is now \($1.full)") } 102 | ramsay.value.last = "Bolton" 103 | ``` 104 | 105 | You can remove observers by keeping the subscription object: 106 | 107 | ```swift 108 | var x = Observable(0) 109 | let subscr = x.afterChange += { (_,_) in println("changed") } 110 | // ... 111 | x.afterChange -= subscr 112 | // without operators: x.afterChange.remove(subscr) 113 | ``` 114 | 115 | Invalidating it: 116 | 117 | ```swift 118 | var x = Observable(0) 119 | let subscr = x.afterChange += { (_,_) in println("changed") } 120 | // ... 121 | subscr.invalidate() // will be removed next time event fires 122 | ``` 123 | 124 | Or tie the subscription to object lifetime: 125 | 126 | ```swift 127 | var x = Observable(0) 128 | for _ in 0..1 { 129 | let o = NSObject() // in real-world this would probably be self 130 | x.afterChange.add(owner: o) { (oV, nV) in println("\(oV) -> \(nV)") } 131 | x <- 42 // handler called 132 | } // o deallocated, handler invalidated 133 | x <- -1 // handler not called 134 | ``` 135 | 136 | You can also chain observables (observe "key paths"): 137 | ```swift 138 | class Person { 139 | let firstName: String 140 | var lastName: Observable 141 | var friend: Observable = Observable(nil) 142 | // init(...) { ... } 143 | } 144 | 145 | let me = Person() 146 | var myFriendsName : String? = nil 147 | 148 | // we want to observe my current friend last name 149 | // and get notified with name when the friend or the name changes 150 | chain(me.friend).to{$0?.lastName}.afterChange += { (_, newName) in 151 | myFriendsName = newName 152 | } 153 | 154 | // alternatively, we can do the same with '/' operator 155 | (me.friend / {$0?.lastName}).afterChange += { (_, newName) in 156 | myFriendsName = newName 157 | } 158 | ``` 159 | 160 | `Event` is a simple `struct` allowing you to define subscribable events. `Observable` uses `EventReference>` for `afterChange` and `beforeChange`. 161 | 162 | ```swift 163 | class SomeClass { 164 | // defining an event someone might be interested in 165 | var somethingChanged = Event() 166 | 167 | // ... 168 | 169 | func doSomething() { 170 | // ... 171 | // fire the event and notify all observers 172 | somethingChanged.notify("Hello!") 173 | // ... 174 | } 175 | } 176 | 177 | var obj = SomeClass() 178 | 179 | // subscribe to an event 180 | obj.somethingChanged += { println($0) } 181 | 182 | obj.doSomething() 183 | ``` 184 | 185 | More examples can be found in tests in `ObservableTests.swift` 186 | 187 | ## Advanced 188 | 189 | If you require observables as reference types, you can use either `ObservableProxy` which is a reference type in between your code and the real `Observable` value type. You can also use `ObservableReference` which is a `ObservableProxy` to an `Observable` that it holds on a property. 190 | 191 | Same is true for `Event`, there is `EventReference` as well. Actually, `Observable` uses `EventReference` instead of `Event`, otherwise some use cases would be difficult to implement. This means, that if you want to unshare events and subscriptions you need to call `observable.unshare(removeSubscriptions:)`. 192 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Observable-Swift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.7.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Observable-Swift/Observable-Swift-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Observable_Swift : NSObject 3 | @end 4 | @implementation PodsDummy_Observable_Swift 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Observable-Swift/Observable-Swift-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #endif 4 | 5 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Observable-Swift/Observable-Swift-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #endif 4 | 5 | 6 | FOUNDATION_EXPORT double ObservableVersionNumber; 7 | FOUNDATION_EXPORT const unsigned char ObservableVersionString[]; 8 | 9 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Observable-Swift/Observable-Swift.modulemap: -------------------------------------------------------------------------------- 1 | framework module Observable { 2 | umbrella header "Observable-Swift-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Observable-Swift/Observable-Swift.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/Observable-Swift 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" "-suppress-warnings" 5 | PODS_BUILD_DIR = $BUILD_DIR 6 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Observable-Swift 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2014 Leszek Ślażyński 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2014 Leszek Ślażyński 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | Observable-Swift 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_SwiftScraper : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_SwiftScraper 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 5 | 6 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 7 | > "$RESOURCES_TO_COPY" 8 | 9 | XCASSET_FILES=() 10 | 11 | case "${TARGETED_DEVICE_FAMILY}" in 12 | 1,2) 13 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 14 | ;; 15 | 1) 16 | TARGET_DEVICE_ARGS="--target-device iphone" 17 | ;; 18 | 2) 19 | TARGET_DEVICE_ARGS="--target-device ipad" 20 | ;; 21 | *) 22 | TARGET_DEVICE_ARGS="--target-device mac" 23 | ;; 24 | esac 25 | 26 | install_resource() 27 | { 28 | if [[ "$1" = /* ]] ; then 29 | RESOURCE_PATH="$1" 30 | else 31 | RESOURCE_PATH="${PODS_ROOT}/$1" 32 | fi 33 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 34 | cat << EOM 35 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 36 | EOM 37 | exit 1 38 | fi 39 | case $RESOURCE_PATH in 40 | *.storyboard) 41 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 42 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 43 | ;; 44 | *.xib) 45 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 46 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 47 | ;; 48 | *.framework) 49 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 50 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 51 | echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 52 | rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 53 | ;; 54 | *.xcdatamodel) 55 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" 56 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 57 | ;; 58 | *.xcdatamodeld) 59 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" 60 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 61 | ;; 62 | *.xcmappingmodel) 63 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" 64 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 65 | ;; 66 | *.xcassets) 67 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 68 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 69 | ;; 70 | *) 71 | echo "$RESOURCE_PATH" 72 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 73 | ;; 74 | esac 75 | } 76 | 77 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 78 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 79 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 80 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 81 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 82 | fi 83 | rm -f "$RESOURCES_TO_COPY" 84 | 85 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] 86 | then 87 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 88 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 89 | while read line; do 90 | if [[ $line != "${PODS_ROOT}*" ]]; then 91 | XCASSET_FILES+=("$line") 92 | fi 93 | done <<<"$OTHER_XCASSETS" 94 | 95 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | fi 97 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #endif 4 | 5 | 6 | FOUNDATION_EXPORT double Pods_SwiftScraperVersionNumber; 7 | FOUNDATION_EXPORT const unsigned char Pods_SwiftScraperVersionString[]; 8 | 9 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift/Observable.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Observable" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_SwiftScraper { 2 | umbrella header "Pods-SwiftScraper-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift/Observable.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Observable" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Observable-Swift 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2014 Leszek Ślażyński 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2014 Leszek Ślażyński 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | Observable-Swift 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_SwiftScraperTests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_SwiftScraperTests 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 5 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 6 | 7 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 8 | 9 | install_framework() 10 | { 11 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 12 | local source="${BUILT_PRODUCTS_DIR}/$1" 13 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 14 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 15 | elif [ -r "$1" ]; then 16 | local source="$1" 17 | fi 18 | 19 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 20 | 21 | if [ -L "${source}" ]; then 22 | echo "Symlinked..." 23 | source="$(readlink "${source}")" 24 | fi 25 | 26 | # use filter instead of exclude so missing patterns dont' throw errors 27 | echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 28 | rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 29 | 30 | local basename 31 | basename="$(basename -s .framework "$1")" 32 | binary="${destination}/${basename}.framework/${basename}" 33 | if ! [ -r "$binary" ]; then 34 | binary="${destination}/${basename}" 35 | fi 36 | 37 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 38 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 39 | strip_invalid_archs "$binary" 40 | fi 41 | 42 | # Resign the code if required by the build settings to avoid unstable apps 43 | code_sign_if_enabled "${destination}/$(basename "$1")" 44 | 45 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 46 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 47 | local swift_runtime_libs 48 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 49 | for lib in $swift_runtime_libs; do 50 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 51 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 52 | code_sign_if_enabled "${destination}/${lib}" 53 | done 54 | fi 55 | } 56 | 57 | # Signs a framework with the provided identity 58 | code_sign_if_enabled() { 59 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 60 | # Use the current code_sign_identitiy 61 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 62 | echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements \"$1\"" 63 | /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements "$1" 64 | fi 65 | } 66 | 67 | # Strip invalid architectures 68 | strip_invalid_archs() { 69 | binary="$1" 70 | # Get architectures for current file 71 | archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" 72 | stripped="" 73 | for arch in $archs; do 74 | if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then 75 | # Strip non-valid architectures in-place 76 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 77 | stripped="$stripped $arch" 78 | fi 79 | done 80 | if [[ "$stripped" ]]; then 81 | echo "Stripped $binary of architectures:$stripped" 82 | fi 83 | } 84 | 85 | 86 | if [[ "$CONFIGURATION" == "Debug" ]]; then 87 | install_framework "$BUILT_PRODUCTS_DIR/Observable-Swift/Observable.framework" 88 | fi 89 | if [[ "$CONFIGURATION" == "Release" ]]; then 90 | install_framework "$BUILT_PRODUCTS_DIR/Observable-Swift/Observable.framework" 91 | fi 92 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 5 | 6 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 7 | > "$RESOURCES_TO_COPY" 8 | 9 | XCASSET_FILES=() 10 | 11 | case "${TARGETED_DEVICE_FAMILY}" in 12 | 1,2) 13 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 14 | ;; 15 | 1) 16 | TARGET_DEVICE_ARGS="--target-device iphone" 17 | ;; 18 | 2) 19 | TARGET_DEVICE_ARGS="--target-device ipad" 20 | ;; 21 | *) 22 | TARGET_DEVICE_ARGS="--target-device mac" 23 | ;; 24 | esac 25 | 26 | install_resource() 27 | { 28 | if [[ "$1" = /* ]] ; then 29 | RESOURCE_PATH="$1" 30 | else 31 | RESOURCE_PATH="${PODS_ROOT}/$1" 32 | fi 33 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 34 | cat << EOM 35 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 36 | EOM 37 | exit 1 38 | fi 39 | case $RESOURCE_PATH in 40 | *.storyboard) 41 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 42 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 43 | ;; 44 | *.xib) 45 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 46 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 47 | ;; 48 | *.framework) 49 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 50 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 51 | echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 52 | rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 53 | ;; 54 | *.xcdatamodel) 55 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" 56 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 57 | ;; 58 | *.xcdatamodeld) 59 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" 60 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 61 | ;; 62 | *.xcmappingmodel) 63 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" 64 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 65 | ;; 66 | *.xcassets) 67 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 68 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 69 | ;; 70 | *) 71 | echo "$RESOURCE_PATH" 72 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 73 | ;; 74 | esac 75 | } 76 | 77 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 78 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 79 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 80 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 81 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 82 | fi 83 | rm -f "$RESOURCES_TO_COPY" 84 | 85 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] 86 | then 87 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 88 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 89 | while read line; do 90 | if [[ $line != "${PODS_ROOT}*" ]]; then 91 | XCASSET_FILES+=("$line") 92 | fi 93 | done <<<"$OTHER_XCASSETS" 94 | 95 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | fi 97 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #endif 4 | 5 | 6 | FOUNDATION_EXPORT double Pods_SwiftScraperTestsVersionNumber; 7 | FOUNDATION_EXPORT const unsigned char Pods_SwiftScraperTestsVersionString[]; 8 | 9 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift/Observable.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Observable" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_SwiftScraperTests { 2 | umbrella header "Pods-SwiftScraperTests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Observable-Swift/Observable.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Observable" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftScraper 2 | 3 | ![](https://api.travis-ci.org/cweatureapps/SwiftScraper.svg?branch=master) 4 | 5 | Web scraping library for Swift. 6 | 7 | # Overview 8 | 9 | This framework provides a simple way to declaratively define a series of steps in Swift that represent how to scrape a web site, allowing the app to read this web page data. 10 | 11 | # Features 12 | 13 | * Declarative API - clearly define the steps to run and avoid the spaghetti 🍝 code that comes with using the WebView and the delegate pattern 14 | * Custom JavaScript integration - Simple integration with custom JavaScript to perform complicated scraping, using the language of the web to process the web page 15 | * Perform custom processing at each step 16 | * Passing data between steps 17 | * Control flow to determine which step to run next, allowing basic conditionals and loops 18 | 19 | # Example 20 | 21 | If you want to read the finished code example straight away: 22 | 23 | * [Swift code that performs a google text search](https://github.com/cweatureapps/SwiftScraper/blob/master/Example/SwiftScraperExample/TutorialViewController.swift) 24 | * [Swift code that performs a google image search, and scrolls to count all images](https://github.com/cweatureapps/SwiftScraper/blob/master/Example/SwiftScraperExample/AdvancedTutorialViewController.swift) 25 | * [The Javascript used in the above examples](https://github.com/cweatureapps/SwiftScraper/blob/master/Example/SwiftScraperExample/GoogleSearch.js) 26 | 27 | For a step by step guide on how to implement these, read the next two sections. 28 | 29 | 30 | # Tutorial 31 | 32 | In this tutorial, we'll cover the basic usage of this framework by performing a search on the google web site. 33 | 34 | ## CocoaPod integration 35 | 36 | Reference this pod in your Podfile: 37 | ```ruby 38 | pod "SwiftScraper", git: "https://github.com/cweatureapps/SwiftScraper.git" 39 | ``` 40 | ## JavaScript setup 41 | 42 | By convention, all the steps will use the functions exposed in a single module which is defined in a single JavaScript file. 43 | 44 | For this exercise, create a new file, `GoogleSearch.js` 45 | 46 | Start by creating the blank JavaScript module structure, making sure the module name is the same as the file name: 47 | 48 | ```javascript 49 | var GoogleSearch = (function() { 50 | return { 51 | }; 52 | })() 53 | ``` 54 | 55 | ## Loading a web page 56 | 57 | Create a new view controller. 58 | 59 | Import the framework: 60 | 61 | ```swift 62 | import SwiftScraper 63 | ``` 64 | 65 | In the view controller, we'll create a step and run it: 66 | 67 | ```swift 68 | var stepRunner: StepRunner! 69 | 70 | override func viewDidLoad() { 71 | super.viewDidLoad() 72 | let step1 = OpenPageStep(path: "https://www.google.com") 73 | stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1]) 74 | stepRunner.insertWebViewIntoView(parent: view) 75 | stepRunner.run() 76 | } 77 | 78 | ``` 79 | 80 | When you run this, you will see a web view opening the Google home page. 81 | 82 | > The web view typically needs to be have a visible frame size, because web sites often use responsive breakpoints and will even sometimes change the HTML structure based on the dimensions of the page. 83 | > 84 | > The `insertWebViewIntoView` method helps you to easily insert the web view into any `UIView` that you have. It is up to you to set up the dimensions of the parent view, or you can even hide it where the user cannot see it. 85 | 86 | ## Check that the page loaded 87 | 88 | We can add an assertion to run some JavaScript code when the page loads, to make sure the page that loaded is expected. We can do this by referencing a JavaScript function which is exposed by the module. 89 | 90 | In the `GoogleSearch.js` file, add the following function which will just check the title of the page is correct. 91 | 92 | ```javascript 93 | var GoogleSearch = (function() { 94 | function assertGoogleTitle() { 95 | return document.title == "Google"; 96 | } 97 | return { 98 | assertGoogleTitle: assertGoogleTitle 99 | }; 100 | })() 101 | ``` 102 | 103 | In the view controller where you created the step, include the name of the assertion function: 104 | 105 | ```swift 106 | let step1 = OpenPageStep( 107 | path: "https://www.google.com", 108 | assertionName: "assertGoogleTitle") 109 | 110 | ``` 111 | 112 | > The assertion function runs immediately when the page loads. 113 | > Sometimes, what you are asserting may not be ready at the point when the page loads, 114 | > as the website may modify the page asynchronously after loading. 115 | 116 | ## Observe progress of the run 117 | 118 | You can observe the progress of the execution by observing the `state` property of the StepRunner object. 119 | 120 | ```swift 121 | stepRunner.state.afterChange.add { change in 122 | print("-----", change.newValue, "-----") 123 | switch change.newValue { 124 | case .inProgress(let index): 125 | print("About to run step at index", index) 126 | case .failure(let error): 127 | print("Failed: ", error.localizedDescription) 128 | case .success: 129 | print("Finished successfully") 130 | default: 131 | break 132 | } 133 | } 134 | stepRunner.run() 135 | ``` 136 | 137 | ## Run script that loads page 138 | 139 | Let's now run some custom JavaScript to submit a google search. This is the `PageChangeStep` which runs some JavaScript, which will result in a new page being loaded. When the page is loaded, it will proceed to the next step. 140 | 141 | Firstly, in the `GoogleSearch.js` file, add the following 2 functions which perform the search, and exposes them in the module: 142 | 143 | ```javascript 144 | var GoogleSearch = (function() { 145 | 146 | // ... 147 | 148 | function performSearch(searchText) { 149 | document.querySelector('input[type="text"], input[type="Search"]').value = searchText; 150 | document.forms[0].submit(); 151 | } 152 | function assertSearchResultTitle() { 153 | return document.title == "SwiftScraper iOS - Google Search"; 154 | } 155 | return { 156 | assertGoogleTitle: assertGoogleTitle, 157 | performSearch: performSearch, 158 | assertSearchResultTitle: assertSearchResultTitle 159 | }; 160 | })() 161 | ``` 162 | 163 | In the view controller, add step 2 which is the `PageChangeStep`, referencing the JavaScript functions you just implemented: 164 | 165 | ```swift 166 | let step2 = PageChangeStep( 167 | functionName: "performSearch", 168 | params: "SwiftScraper iOS", 169 | assertionName: "assertSearchResultTitle") 170 | 171 | ``` 172 | 173 | Notice the `params` parameter in the initializer, which allows you to pass data to the JavaScript function. 174 | 175 | Make sure to include this in the array of steps when you create the `StepRunner`: 176 | 177 | ```swift 178 | stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1, step2]) 179 | ``` 180 | 181 | ## Run script and process 182 | 183 | We're at the last step - we can run a script to scrape the contents of the page. Add the following JavaScript function which will get the search results, and return an Array of JSON objects with the text and href of each link. 184 | 185 | ```javascript 186 | var GoogleSearch = (function() { 187 | 188 | // ... 189 | 190 | function getSearchResults() { 191 | var headings = document.querySelectorAll('h3.r'); 192 | return Array.prototype.slice.call(headings).map(function (h3) { 193 | return { 'text': h3.innerText, 'href': h3.childNodes[0].href }; 194 | }); 195 | } 196 | 197 | return { 198 | assertGoogleTitle: assertGoogleTitle, 199 | performSearch: performSearch, 200 | assertSearchResultTitle: assertSearchResultTitle, 201 | getSearchResults: getSearchResults 202 | }; 203 | })() 204 | ``` 205 | 206 | In the Swift code, add the 3rd step which is a `ScriptStep`, a step which runs a JavaScript function and returns the response that the function returns. 207 | 208 | ```swift 209 | let step3 = ScriptStep(functionName: "getSearchResults") { response, _ in 210 | if let responseArray = response as? [JSON] { 211 | responseArray.forEach { json in 212 | if let text = json["text"], let href = json["href"] { 213 | print(text, "(", href, ")") 214 | } 215 | } 216 | } 217 | return .proceed 218 | } 219 | ``` 220 | 221 | And make sure to include this in the array of steps when you create the `StepRunner`: 222 | 223 | ```swift 224 | stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1, step2, step3]) 225 | ``` 226 | 227 | Run this. You should see the steps complete successfully, and print the search results to the console. 228 | 229 | Congratulations! 🎉 You've finished the tutorial on the basic usage of this library! 👍 230 | 231 | # Advanced Usage 232 | 233 | ## Run script that returns data async 234 | 235 | It is possible to run some JavaScript that does not return immediately, and wait for it to asynchronously call back the Swift code after some time has passed. For example, you may need to do something on the web page, poll for the operation to complete, and then pass the data back to Swift. 236 | 237 | To pass data back to Swift world, call `SwiftScraper.postMessage()`, passing a single object that can be serialized back to a Swift object. 238 | 239 | In this example, we'll do a google image search, and then scroll down to the bottom. The infinite scroll pattern employed here will load more images when we do this, and we'll do a count of the images before and after the scroll. 240 | 241 | ```javascript 242 | var GoogleSearch = (function() { 243 | 244 | // ... 245 | 246 | function scrollAndCountImages() { 247 | var firstCount = document.querySelectorAll('img').length; 248 | window.scrollTo(0, document.body.scrollHeight); 249 | setTimeout(function () { 250 | var secondCount = document.querySelectorAll('img').length; 251 | SwiftScraper.postMessage({'first': firstCount, 'second': secondCount}); 252 | }, 2000); 253 | } 254 | 255 | return { 256 | assertGoogleTitle: assertGoogleTitle, 257 | performSearch: performSearch, 258 | assertSearchResultTitle: assertSearchResultTitle, 259 | getSearchResults: getSearchResults, 260 | scrollAndCountImages: scrollAndCountImages 261 | }; 262 | })() 263 | ``` 264 | 265 | > For those familiar with `WKWebView`, the `SwiftScraper.postMessage()` function is just an alias for 266 | > `webkit.messageHandlers.swiftScraperResponseHandler.postMessage()` 267 | 268 | In Swift, use the `AsyncScriptStep`, which is used in the same way as `ScriptStep`, with the difference being the handler is not called until `SwiftScraper.postMessage` is called. It is expected that the JavaScript function itself does not return anything. 269 | 270 | ```swift 271 | let step1 = OpenPageStep(path: "https://www.google.com.au/search?tbm=isch") 272 | 273 | let step2 = PageChangeStep( 274 | functionName: "performSearch", 275 | params: "ankylosaurus") 276 | 277 | let step3 = AsyncScriptStep(functionName: "scrollAndCountImages") { response, _ in 278 | if let json = response as? JSON { 279 | if let first = json["first"], let second = json["second"] { 280 | print("first: ", first, "second: ", second) 281 | } 282 | } 283 | return .proceed 284 | } 285 | ``` 286 | 287 | ## Process Step 288 | Use the `ProcessStep` when you need a step that requires some custom action to be performed. 289 | 290 | ```swift 291 | let processStep = ProcessStep { model in 292 | // perform some custom action here 293 | return .proceed 294 | } 295 | ``` 296 | 297 | Two main concepts to note here are: 298 | * The `model` parameter, used for passing model data between steps 299 | * The return value, which can be used for control flow 300 | 301 | These concepts apply to the `ProcessStep`, `ScriptStep` and `AsyncScriptStep`. We'll explore them in the next two sections. 302 | 303 | ## Passing model data 304 | The `ProcessStep`, `ScriptStep` and `AsyncScriptStep` all have a handler closure to perform processing, and these handlers all have a model parameter of type `inout JSON`. Modify this JSON dictionary to save data during one step, and then read it in another step. 305 | 306 | Let's modify the `AsyncScriptStep` from the previous section to save the before and after counts to the dictionary. 307 | 308 | ```swift 309 | let step3 = AsyncScriptStep(functionName: "scrollAndCountImages") { response, model in // notice the model param 310 | if let json = response as? JSON { 311 | if let first = json["first"], let second = json["second"] { 312 | print("first: ", first, "second: ", second) 313 | 314 | // Save the data to the model dictionary 315 | model["first"] = first 316 | model["second"] = second 317 | } 318 | } 319 | return .proceed 320 | } 321 | ``` 322 | 323 | ## Control Flow 324 | 325 | The return value is an enum which can be used for rudimentary control flow. We've seen `.proceed` which means to go to the next step. The `.jumpToStep(n)` allows you to jump to another step, either before or after the current step. This allows you to define loops (by jumping back) as well as conditionals (by jumping forward). 326 | 327 | Let's continue the infinite scrolling image search example, and add a `ProcessStep` that will keep looping back to `step3` until the before count and after count are the same, meaning there are no more images on the page to load. 328 | 329 | Add this step as the last step to run. When you run this, you should see the screen keep scrolling down until no more images can be found. 330 | 331 | ```swift 332 | let conditionStep = ProcessStep { model in 333 | if let first = model["first"] as? Int, 334 | let second = model["second"] as? Int, 335 | first == second { 336 | return .proceed 337 | } else { 338 | return .jumpToStep(2) // This is a zero-based index, i.e. step3 339 | } 340 | } 341 | ``` 342 | 343 | > This technique is most useful for repeating a sequence of steps. 344 | > While it can also be used to model IF-THEN style conditionals, it is essentially a `GOTO` construct 345 | > and can easily lead to unmaintainable spaghetti 🍝 steps. 346 | 347 | You can also have an early exit from the steps. The return value of `.finish` will stop execution as a success, while `.failure(Error)` will stop execution with a failure. 348 | 349 | 350 | ## Wait Step 351 | A step that waits for a set period of time. 352 | 353 | ```swift 354 | let waitStep = WaitStep(waitTimeInSeconds: 0.5) 355 | ``` 356 | 357 | ## Wait for Condition Step 358 | This is a step that waits for a condition to become true before proceeding, or it will fail if the condition is still false when the timeout occurs. 359 | 360 | In this example, the iOS code will repeatedly call the JavaScript function `testThatStuffIsReady`, proceeding as soon as it returns true, or failing with a timeout if it doesn't return true within 2 seconds. 361 | 362 | ```swift 363 | let waitForConditionStep = WaitForConditionStep( 364 | assertionName: "testThatStuffIsReady", 365 | timeoutInSeconds: 2) 366 | ``` 367 | 368 | # FAQ 369 | 370 | ***I'm getting the error: "An SSL error has occurred and a secure connection to the server cannot be made."*** 371 | 372 | App Transport Security (ATS) rules apply web views as well. If the website you are loading is not HTTPS, or uses outdated security protocols, then iOS will refuse to load it. 373 | 374 | The quick workaround is to disable ATS by putting the following setting in your `Info.plist` 375 | 376 | ``` 377 | NSAppTransportSecurity 378 | 379 | NSAllowsArbitraryLoads 380 | 381 | 382 | ``` 383 | 384 | However, at some point in the future, Apple may require that all apps submitted to the App Store support ATS. 385 | 386 | For more information, see the following links: 387 | 388 | * [https://forums.developer.apple.com/thread/6767](https://forums.developer.apple.com/thread/6767) 389 | * [https://developer.apple.com/news/?id=12212016b](https://developer.apple.com/news/?id=12212016b) 390 | * [https://developer.apple.com/videos/play/wwdc2016/706/](https://developer.apple.com/news/?id=12212016b) 391 | 392 | 393 | # License 394 | 395 | SwiftScraper is available under the MIT license. See the LICENSE file for more info. 396 | -------------------------------------------------------------------------------- /Resources/SwiftScraper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Core JavaScript helper functions for SwiftScraper. 3 | */ 4 | var SwiftScraper = (function() { 5 | /** 6 | * Posts a message back to iOS WebView. 7 | * This is a shortcut for the webkit messageHandlers postMessage function. 8 | */ 9 | function postMessage(message) { 10 | window.webkit.messageHandlers.swiftScraperResponseHandler.postMessage(message); 11 | } 12 | return { 13 | postMessage: postMessage 14 | }; 15 | })(); 16 | -------------------------------------------------------------------------------- /Sources/Browser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Browser.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import WebKit 11 | 12 | // MARK: - Types 13 | 14 | /// The result of the browser navigation. 15 | typealias NavigationResult = Result 16 | 17 | /// Invoked when the page navigation has completed or failed. 18 | typealias NavigationCallback = (_ result: NavigationResult) -> Void 19 | 20 | /// The result of some JavaScript execution. 21 | /// 22 | /// If successful, it contains the response from the JavaScript; 23 | /// If it failed, it contains the error. 24 | typealias ScriptResponseResult = Result 25 | 26 | /// Invoked when the asynchronous call to some JavaScript is completed, containing the response or error. 27 | typealias ScriptResponseResultCallback = (_ result: ScriptResponseResult) -> Void 28 | 29 | // MARK: - Browser 30 | 31 | /// The browser used to perform the web scraping. 32 | /// 33 | /// This class encapsulates the webview and its delegates, providing an closure based API. 34 | public class Browser: NSObject, WKNavigationDelegate, WKScriptMessageHandler { 35 | 36 | // MARK: - Constants 37 | 38 | private enum Constants { 39 | static let coreScript = "SwiftScraper" 40 | static let messageHandlerName = "swiftScraperResponseHandler" 41 | } 42 | 43 | // MARK: - Properties 44 | 45 | private let moduleName: String 46 | private (set) public var webView: WKWebView! 47 | private let userContentController = WKUserContentController() 48 | private var navigationCallback: NavigationCallback? 49 | private var asyncScriptCallback: ScriptResponseResultCallback? 50 | 51 | // MARK: - Setup 52 | 53 | /// Initialize the Browser object. 54 | /// 55 | /// - parameter moduleName: The name of the JavaScript module. By convention, the filename of the JavaScript file is the same as the module name. 56 | /// - parameter scriptBundle: The bundle from which to load the JavaScript file. Defaults to the main bundle. 57 | /// - parameter customUserAgent: The custom user agent string (only works for iOS 9+). 58 | init(moduleName: String, scriptBundle: Bundle = Bundle.main, customUserAgent: String? = nil) { 59 | self.moduleName = moduleName 60 | super.init() 61 | setupWebView(moduleName: moduleName, customUserAgent: customUserAgent, scriptBundle: scriptBundle) 62 | } 63 | 64 | private func setupWebView(moduleName: String, customUserAgent: String?, scriptBundle: Bundle) { 65 | 66 | let coreScriptURL = moduleResourceBundle().path(forResource: Constants.coreScript, ofType: "js") 67 | let coreScriptContent = try! String(contentsOfFile: coreScriptURL!) 68 | let coreScript = WKUserScript(source: coreScriptContent, injectionTime: .atDocumentEnd, forMainFrameOnly: true) 69 | userContentController.addUserScript(coreScript) 70 | 71 | let moduleScriptURL = scriptBundle.path(forResource: moduleName, ofType: "js") 72 | let moduleScriptContent = try! String(contentsOfFile: moduleScriptURL!) // TODO: prevent force try, propagate error 73 | let moduleScript = WKUserScript(source: moduleScriptContent, injectionTime: .atDocumentEnd, forMainFrameOnly: true) 74 | userContentController.addUserScript(moduleScript) 75 | 76 | userContentController.add(self, name: Constants.messageHandlerName) 77 | 78 | let config = WKWebViewConfiguration() 79 | config.userContentController = userContentController 80 | 81 | webView = WKWebView(frame: CGRect.zero, configuration: config) 82 | webView.navigationDelegate = self 83 | webView.allowsBackForwardNavigationGestures = true 84 | if #available(iOS 9.0, *) { 85 | webView.customUserAgent = customUserAgent 86 | } 87 | } 88 | 89 | /// Returns the resource bundle for this Pod where all the resources are kept, 90 | /// or defaulting to the framework module bundle (e.g. when running unit tests). 91 | private func moduleResourceBundle() -> Bundle { 92 | let moduleBundle = Bundle(for: Browser.self) 93 | guard let resourcesBundleURL = moduleBundle.url(forResource: "SwiftScraper", withExtension: ".bundle"), 94 | let resourcesBundle = Bundle(url: resourcesBundleURL) else { 95 | return moduleBundle 96 | } 97 | return resourcesBundle 98 | } 99 | 100 | // MARK: - WKNavigationDelegate 101 | 102 | public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 103 | print("didFinishNavigation was called") 104 | callNavigationCompletion(result: .success(())) 105 | } 106 | 107 | public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 108 | print("didFailProvisionalNavigation") 109 | let navigationError = SwiftScraperError.navigationFailed(error: error) 110 | callNavigationCompletion(result: .failure(navigationError)) 111 | } 112 | 113 | public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { 114 | print("didFailNavigation was called") 115 | let nsError = error as NSError 116 | if nsError.domain == "NSURLErrorDomain" && nsError.code == NSURLErrorCancelled { 117 | return 118 | } 119 | let navigationError = SwiftScraperError.navigationFailed(error: error) 120 | callNavigationCompletion(result: .failure(navigationError)) 121 | } 122 | 123 | private func callNavigationCompletion(result: NavigationResult) { 124 | guard let navigationCompletion = self.navigationCallback else { return } 125 | // Make a local copy of closure before setting to nil, to due async nature of this, 126 | // there is a timing issue if simply setting to nil after calling the completion. 127 | // This is because the completion is the code that triggers the next step. 128 | self.navigationCallback = nil 129 | navigationCompletion(result) 130 | } 131 | 132 | // MARK: - WKScriptMessageHandler 133 | 134 | public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 135 | print("WKScriptMessage didReceiveMessage") 136 | guard message.name == Constants.messageHandlerName else { 137 | print("Ignoring message with name of \(message.name)") 138 | return 139 | } 140 | asyncScriptCallback?(.success(message.body)) 141 | } 142 | 143 | // MARK: - API 144 | 145 | /// Insert the WebView at index 0 of the given parent view, 146 | /// using AutoLayout to pin all 4 sides to the parent. 147 | func insertIntoView(parent: UIView) { 148 | parent.insertSubview(webView, at: 0) 149 | webView.translatesAutoresizingMaskIntoConstraints = false 150 | if #available(iOS 9.0, *) { 151 | webView.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true 152 | webView.bottomAnchor.constraint(equalTo: parent.bottomAnchor).isActive = true 153 | webView.leadingAnchor.constraint(equalTo: parent.leadingAnchor).isActive = true 154 | webView.trailingAnchor.constraint(equalTo: parent.trailingAnchor).isActive = true 155 | } else { 156 | NSLayoutConstraint(item: webView, attribute: .top, relatedBy: .equal, toItem: parent, attribute: .top, multiplier: 1.0, constant: 0).isActive = true 157 | NSLayoutConstraint(item: webView, attribute: .bottom, relatedBy: .equal, toItem: parent, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true 158 | NSLayoutConstraint(item: webView, attribute: .leading, relatedBy: .equal, toItem: parent, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true 159 | NSLayoutConstraint(item: webView, attribute: .trailing, relatedBy: .equal, toItem: parent, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true 160 | } 161 | } 162 | 163 | /// Loads a page with the given path into the WebView. 164 | func load(path: String, completion: @escaping NavigationCallback) { 165 | self.navigationCallback = completion 166 | webView.load(URLRequest(url: URL(string: path)!)) 167 | } 168 | 169 | /// Run some JavaScript with error handling and logging. 170 | func runScript(functionName: String, params: [Any] = [], completion: @escaping ScriptResponseResultCallback) { 171 | guard let script = try? JavaScriptGenerator.generateScript(moduleName: moduleName, functionName: functionName, params: params) else { 172 | completion(.failure(SwiftScraperError.parameterSerialization)) 173 | return 174 | } 175 | print("script to run:", script) 176 | webView.evaluateJavaScript(script) { response, error in 177 | if let nsError = error as NSError?, 178 | nsError.domain == WKError.errorDomain, 179 | nsError.code == WKError.Code.javaScriptExceptionOccurred.rawValue { 180 | let jsErrorMessage = nsError.userInfo["WKJavaScriptExceptionMessage"] as? String ?? nsError.localizedDescription 181 | print("javaScriptExceptionOccurred error: \(jsErrorMessage)") 182 | completion(.failure(SwiftScraperError.javascriptError(errorMessage: jsErrorMessage))) 183 | } else if let error = error { 184 | print("javascript error: \(error.localizedDescription)") 185 | completion(.failure(SwiftScraperError.javascriptError(errorMessage: error.localizedDescription))) 186 | } else { 187 | print("javascript response:") 188 | print(response ?? "(no response)") 189 | completion(.success(response)) 190 | } 191 | } 192 | } 193 | 194 | /// Run some JavaScript that results in a page being loaded (i.e. navigation happens). 195 | func runPageChangeScript(functionName: String, params: [Any] = [], completion: @escaping NavigationCallback) { 196 | self.navigationCallback = completion 197 | runScript(functionName: functionName, params: params) { result in 198 | if case .failure(let error) = result { 199 | completion(.failure(error)) 200 | self.navigationCallback = nil 201 | } 202 | } 203 | } 204 | 205 | /// Run some JavaScript asynchronously, the completion being called when a script message is received from the JavaScript. 206 | func runAsyncScript(functionName: String, params: [Any] = [], completion: @escaping ScriptResponseResultCallback) { 207 | self.asyncScriptCallback = completion 208 | runScript(functionName: functionName, params: params) { result in 209 | if case .failure = result { 210 | completion(result) 211 | self.asyncScriptCallback = nil 212 | } 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.3.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/JSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 27/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// JSON dictionary. 12 | public typealias JSON = [String: Any] 13 | -------------------------------------------------------------------------------- /Sources/JavaScriptGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JavaScriptGenerator.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 24/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct JavaScriptGenerator { 12 | 13 | static func generateScript(moduleName: String, functionName: String, params: [Any] = []) throws -> String { 14 | guard !params.isEmpty else { 15 | return "\(moduleName).\(functionName)()" 16 | } 17 | 18 | let args = try params.map { try stringify(param: $0) } 19 | let argsJoined = args.joined(separator: ",") 20 | return "\(moduleName).\(functionName)(\(argsJoined))" 21 | } 22 | 23 | private static func stringify(param: Any) throws -> String { 24 | if let s = param as? String { 25 | return "\"\(s)\"" 26 | } else if let b = param as? Bool { 27 | return b ? "true" : "false" 28 | } else if param is Int || param is Double { 29 | return "\(param)" 30 | } else if param is NSNull { 31 | return "null" 32 | } else if JSONSerialization.isValidJSONObject(param), 33 | let prettyJsonData = try? JSONSerialization.data(withJSONObject: param, options: []), 34 | let jsonString = NSString(data: prettyJsonData, encoding: String.Encoding.utf8.rawValue) as String? { 35 | return jsonString 36 | } 37 | throw SwiftScraperError.parameterSerialization 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Result { 12 | case success(T) 13 | case failure(E) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/StepRunner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepRunner.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Observable 11 | 12 | // MARK: - StepRunnerState 13 | 14 | /// Indicates the progress and status of the `StepRunner`. 15 | public enum StepRunnerState { 16 | /// Not yet started, `run()` has not been called. 17 | case notStarted 18 | 19 | /// The pipeline is running, and currently executing the step at the index. 20 | case inProgress(index: Int) 21 | 22 | /// The execution finished successfully. 23 | case success 24 | 25 | /// The execution failed with the given error. 26 | case failure(error: Error) 27 | } 28 | 29 | public func == (lhs: StepRunnerState, rhs: StepRunnerState) -> Bool { 30 | switch (lhs, rhs) { 31 | case (.notStarted, .notStarted): return true 32 | case (.success, .success): return true 33 | case (.failure, .failure): return true 34 | case (.inProgress(let lhsIndex), .inProgress(let rhsIndex)): 35 | return lhsIndex == rhsIndex 36 | default: return false 37 | } 38 | } 39 | 40 | public func != (lhs: StepRunnerState, rhs: StepRunnerState) -> Bool { 41 | return !(lhs == rhs) 42 | } 43 | 44 | // MARK: - StepRunner 45 | 46 | /// The `StepRunner` is the engine that runs the steps in the pipeline. 47 | /// 48 | /// Once initialized, call the `run()` method to execute the steps, 49 | /// and observe the `state` property to be notified of progress and status. 50 | public class StepRunner { 51 | 52 | /// The observable state which indicates the progress and status. 53 | public private(set) var state: Observable = Observable(.notStarted) 54 | 55 | /// A model dictionary which can be used to pass data from step to step. 56 | public private(set) var model: JSON = [:] 57 | 58 | private let browser: Browser 59 | private var steps: [Step] 60 | private var index = 0 61 | 62 | /// Initializer to create the `StepRunner`. 63 | /// 64 | /// - parameter moduleName: The name of the JavaScript module which has your customer functions. 65 | /// By convention, the filename of the JavaScript file is the same as the module name. 66 | /// - parameter scriptBundle: The bundle from which to load the JavaScript file. Defaults to the main bundle. 67 | /// - parameter customUserAgent: The custom user agent string (only works for iOS 9+). 68 | /// - parameter steps: The steps to run in the pipeline. 69 | public init( 70 | moduleName: String, 71 | scriptBundle: Bundle = Bundle.main, 72 | customUserAgent: String? = nil, 73 | steps: [Step]) { 74 | browser = Browser(moduleName: moduleName, scriptBundle: scriptBundle, customUserAgent: customUserAgent) 75 | self.steps = steps 76 | } 77 | 78 | /// Execute the steps. 79 | public func run() { 80 | guard index < steps.count else { 81 | state ^= .failure(error: SwiftScraperError.incorrectStep) 82 | return 83 | } 84 | let stepToExecute = steps[index] 85 | state ^= .inProgress(index: index) 86 | stepToExecute.run(with: browser, model: model) { [weak self] result in 87 | guard let this = self else { return } 88 | this.model = result.model 89 | switch result { 90 | case .finish: 91 | this.state ^= .success 92 | case .proceed: 93 | this.index += 1 94 | guard this.index < this.steps.count else { 95 | this.state ^= .success 96 | return 97 | } 98 | this.run() 99 | case .jumpToStep(let nextStep, _): 100 | this.index = nextStep 101 | this.run() 102 | case .failure(let error, _): 103 | this.state ^= .failure(error: error) 104 | } 105 | } 106 | } 107 | 108 | /// Resets the existing StepRunner and execute the given steps in the existing browser. 109 | /// 110 | /// Use this to perform more steps on a StepRunner which has previously finished processing. 111 | public func run(steps: [Step]) { 112 | state ^= .notStarted 113 | self.steps = steps 114 | index = 0 115 | run() 116 | } 117 | 118 | /// Insert the WebView used for scraping at index 0 of the given parent view, using AutoLayout to pin all 4 sides to the parent. 119 | /// 120 | /// Useful if the app would like to see the scraping in the foreground. 121 | public func insertWebViewIntoView(parent: UIView) { 122 | browser.insertIntoView(parent: parent) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/Steps/AsyncScriptStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncScriptStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Step that runs some script which will return a result asynchronously via `SwiftScraper.postMessage()`. 12 | /// 13 | /// The `StepFlowResult` returned by the `handler` can be used to drive control flow of the steps. 14 | public class AsyncScriptStep: ScriptStep { 15 | 16 | // Note: Manually override init() due to Swift unsupported warning: 17 | // "Synthesizing a variadic inherited initializer for subclass is unsupported" 18 | // Won't need this if Swift supports it in the future. 19 | 20 | /// Initializer. 21 | /// 22 | /// - parameter functionName: The name of the JavaScript function to call. The module namespace is automatically added. 23 | /// - parameter params: Parameters which will be passed to the JavaScript function. 24 | /// - parameter paramsKeys: Look up the values from the JSON model dictionary using these keys, 25 | /// and pass them as the parameters to the JavaScript function. If provided, these are used instead of `params`. 26 | /// - parameter handler: Callback function which returns data from JavaScript, and passes the model JSON dictionary for modification. 27 | override public init( 28 | functionName: String, 29 | params: Any..., 30 | paramsKeys: [String] = [], 31 | handler: @escaping ScriptStepHandler) { 32 | super.init( 33 | functionName: functionName, 34 | params: [], // Can't pass to super here. e.g. if params is ['a', 3], then the single array with 2 elems would get passed to super.init 35 | paramsKeys: paramsKeys, 36 | handler: handler) 37 | super.params = params // set the params here 38 | } 39 | 40 | override func runScript(browser: Browser, functionName: String, params: [Any], completion: @escaping ScriptResponseResultCallback) { 41 | browser.runAsyncScript(functionName: functionName, params: params, completion: completion) 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Sources/Steps/NavigableStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigableStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Step that will navigate to a new page. 12 | protocol NavigableStep: Step { 13 | 14 | /// Name of JavaScript function that checks whether the page loaded correctly and returns a Boolean. 15 | var assertionName: String? { get set } 16 | } 17 | 18 | extension NavigableStep { 19 | 20 | /// Runs the assertion function to check whether the page loaded correctly and calling the `StepCompletionCallback`. 21 | /// 22 | /// - parameter browser: The `Browser` used for web scraping. 23 | /// - parameter model: A JSON model that allows data to be passed from step to step in the pipeline. 24 | /// - parameter completion: The completion called to indicate success or failure. 25 | func assertNavigation(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) { 26 | guard let assertionName = self.assertionName else { 27 | completion(.proceed(model)) 28 | return 29 | } 30 | browser.runScript(functionName: assertionName) { result in 31 | switch result { 32 | case .success(let ok as Bool) where ok: 33 | completion(.proceed(model)) 34 | default: 35 | completion(.failure(SwiftScraperError.contentUnexpected, model)) 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Steps/OpenPageStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenPageStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Step that loads a new page. 12 | public class OpenPageStep: Step, NavigableStep { 13 | private var path: String 14 | var assertionName: String? 15 | 16 | /// Initializer. 17 | /// 18 | /// - parameter path: The address of the page to load, as you would type into the browser address bar. 19 | /// - parameter assertionName: Name of JavaScript function that checks whether the page loaded correctly. 20 | public init(path: String, assertionName: String? = nil) { 21 | self.path = path 22 | self.assertionName = assertionName 23 | } 24 | 25 | public func run(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) { 26 | browser.load(path: path) { [weak self] result in 27 | guard let this = self else { return } 28 | if case .failure(let error) = result { 29 | completion(.failure(error, model)) 30 | return 31 | } 32 | this.assertNavigation(with: browser, model: model, completion: completion) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Steps/PageChangeStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageChangeStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Step that runs some script, which will result in a new page being loaded. 12 | public class PageChangeStep: Step, NavigableStep { 13 | 14 | private var functionName: String 15 | private var params: [Any] 16 | private var paramsKeys: [String] 17 | var assertionName: String? 18 | 19 | /// Initializer. 20 | /// 21 | /// - parameter functionName: The name of the JavaScript function to call. The module namespace is automatically added. 22 | /// - parameter params: Parameters which will be passed to the JavaScript function. 23 | /// - parameter paramsKeys: Look up the values from the JSON model dictionary using these keys, 24 | /// and pass them as the parameters to the JavaScript function. If provided, these are used instead of `params`. 25 | /// - parameter assertionName: Name of JavaScript function that checks whether the page loaded correctly. 26 | public init( 27 | functionName: String, 28 | params: Any..., 29 | paramsKeys: [String] = [], 30 | assertionName: String? = nil) { 31 | self.functionName = functionName 32 | self.params = params 33 | self.paramsKeys = paramsKeys 34 | self.assertionName = assertionName 35 | } 36 | 37 | public func run(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) { 38 | let params: [Any] 39 | if paramsKeys.isEmpty { 40 | params = self.params 41 | } else { 42 | params = paramsKeys.map { model[$0] ?? NSNull() } 43 | } 44 | browser.runPageChangeScript(functionName: functionName, params: params) { [weak self] result in 45 | guard let this = self else { return } 46 | if case .failure(let error) = result { 47 | completion(.failure(error, model)) 48 | return 49 | } 50 | this.assertNavigation(with: browser, model: model, completion: completion) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Steps/ProcessStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 26/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Types 12 | 13 | /// Handler that allows some custom action to be performed for `ProcessStep`, 14 | /// with the return value used to drive control flow of the steps. 15 | /// 16 | /// - parameter model: The model JSON dictionary which can be modified by the step. 17 | /// - returns: The `StepFlowResult` which allows control flow of the steps. 18 | public typealias ProcessStepHandler = (_ model: inout JSON) -> StepFlowResult 19 | 20 | 21 | // MARK: - ProcessStep 22 | 23 | /// Step that performs some processing, can update the model dictionary, 24 | /// and can be used to drive control flow of the steps. 25 | public class ProcessStep: Step { 26 | 27 | private var handler: ProcessStepHandler 28 | 29 | /// Initializer. 30 | /// 31 | /// - parameter handler: The action to perform in this step. 32 | public init(handler: @escaping ProcessStepHandler) { 33 | self.handler = handler 34 | } 35 | 36 | public func run(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) { 37 | var modelCopy = model 38 | let result = handler(&modelCopy) 39 | completion(result.convertToStepCompletionResult(with: modelCopy)) 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /Sources/Steps/ScriptStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Types 12 | 13 | /// Callback invoked when a `ScriptStep` or `AsyncScriptStep` is finished. 14 | /// 15 | /// - parameter response: Data returned from JavaScript. 16 | /// - parameter model: The model JSON dictionary which can be modified by the step. 17 | /// - returns: The `StepFlowResult` which allows control flow of the steps. 18 | public typealias ScriptStepHandler = (_ response: Any?, _ model: inout JSON) -> StepFlowResult 19 | 20 | // MARK: - ScriptStep 21 | 22 | /// Step that runs some JavaScript which will return a result immediately from the JavaScript function. 23 | /// 24 | /// The `StepFlowResult` returned by the `handler` can be used to drive control flow of the steps. 25 | public class ScriptStep: Step { 26 | private var functionName: String 27 | var params: [Any] 28 | private var paramsKeys: [String] 29 | private var handler: ScriptStepHandler 30 | 31 | /// Initializer. 32 | /// 33 | /// - parameter functionName: The name of the JavaScript function to call. The module namespace is automatically added. 34 | /// - parameter params: Parameters which will be passed to the JavaScript function. 35 | /// - parameter paramsKeys: Look up the values from the JSON model dictionary using these keys, 36 | /// and pass them as the parameters to the JavaScript function. If provided, these are used instead of `params`. 37 | /// - parameter handler: Callback function which returns data from JavaScript, and passes the model JSON dictionary for modification. 38 | public init( 39 | functionName: String, 40 | params: Any..., 41 | paramsKeys: [String] = [], 42 | handler: @escaping ScriptStepHandler) { 43 | self.functionName = functionName 44 | self.params = params 45 | self.paramsKeys = paramsKeys 46 | self.handler = handler 47 | } 48 | 49 | public func run(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) { 50 | let params: [Any] 51 | if paramsKeys.isEmpty { 52 | params = self.params 53 | } else { 54 | params = paramsKeys.map { model[$0] ?? NSNull() } 55 | } 56 | runScript(browser: browser, functionName: functionName, params: params) { [weak self] result in 57 | guard let this = self else { return } 58 | switch result { 59 | case .failure(let error): 60 | completion(.failure(error, model)) 61 | case .success(let response): 62 | var modelCopy = model 63 | let result = this.handler(response, &modelCopy) 64 | completion(result.convertToStepCompletionResult(with: modelCopy)) 65 | } 66 | } 67 | } 68 | 69 | func runScript(browser: Browser, functionName: String, params: [Any], completion: @escaping ScriptResponseResultCallback) { 70 | browser.runScript(functionName: functionName, params: params, completion: completion) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Steps/Step.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Step.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Represents a step which is performed as part of the scraping pipeline flow. 12 | /// The `StepRunner` will execute each step by calling the `run()` method, 13 | /// and using the result of the callback to determine what to do next. 14 | public protocol Step { 15 | 16 | /// Execute the step. 17 | /// 18 | /// When all work is done, the `completion` should be called, 19 | /// and indicate what to do next (i.e. control flow instruction). 20 | /// 21 | /// - parameter browser: The `Browser` used for web scraping. 22 | /// - parameter model: A JSON model that allows data to be passed from step to step in the pipeline. 23 | /// - parameter completion: The completion called to indicate what to do next (i.e. control flow instruction). 24 | /// The JSON model must be passed back here, to pass onto the next step. 25 | func run(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Steps/StepCompletionCallback.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepCompletionCallback.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 27/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Result that indicates what to do next (i.e. control flow instruction) 12 | /// when a step's `run` method is complete. 13 | /// This also contains the model which is passed betweeen the steps. 14 | public enum StepCompletionResult { 15 | /// Proceed to the next step. 16 | case proceed(JSON) 17 | 18 | /// Jump to the step at the given index in the `Step` array and continue execution from there. 19 | case jumpToStep(Int, JSON) 20 | 21 | /// StepRunnerState stops executing, and finishes immediately with a state of `StepRunnerState.success`. 22 | case finish(JSON) 23 | 24 | /// StepRunnerState stops executing, and finishes immediately with a state of `StepRunnerState.failure`. 25 | case failure(Error, JSON) 26 | 27 | /// The associated JSON model for the result. 28 | var model: JSON { 29 | switch self { 30 | case .proceed(let model): return model 31 | case .finish(let model): return model 32 | case .jumpToStep(_, let model): return model 33 | case .failure(_, let model): return model 34 | } 35 | } 36 | } 37 | 38 | /// Callback that should be invoked when the step's `run` method is complete, 39 | /// and can provides instruction on what to do next (e.g. proceed or fail). 40 | /// 41 | /// - parameter result: Result indicating what to do next (i.e. control flow instruction). 42 | /// The JSON model must be provided to pass onto the next step. 43 | public typealias StepCompletionCallback = (_ result: StepCompletionResult) -> Void 44 | -------------------------------------------------------------------------------- /Sources/Steps/StepFlowResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepFlowResult.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 28/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Result which defines control flow of the steps. 12 | public enum StepFlowResult { 13 | /// Proceed to the next step. 14 | case proceed 15 | 16 | /// Jump to the step at the given index in the `Step` array and continue execution from there. 17 | case jumpToStep(Int) 18 | 19 | /// StepRunnerState stops executing, and finishes immediately with a state of `StepRunnerState.success`. 20 | case finish 21 | 22 | /// StepRunnerState stops executing, and finishes immediately with a state of `StepRunnerState.failure`. 23 | case failure(Error) 24 | 25 | /// Converts to a StepCompletionResult. 26 | func convertToStepCompletionResult(with model: JSON) -> StepCompletionResult { 27 | switch self { 28 | case .proceed: 29 | return .proceed(model) 30 | case .finish: 31 | return .finish(model) 32 | case .jumpToStep(let step): 33 | return .jumpToStep(step, model) 34 | case .failure(let error): 35 | return .failure(error, model) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Steps/WaitForConditionStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaitForConditionStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 28/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Step that waits for condition to become true before proceeding, 12 | /// failing if the condition is still false when timeout occurs. 13 | public class WaitForConditionStep: Step { 14 | 15 | private enum Constants { 16 | static let refreshInterval: TimeInterval = 0.1 17 | } 18 | 19 | private var assertionName: String 20 | private var timeoutInSeconds: TimeInterval 21 | private var timer: Timer? 22 | 23 | private var startRunDate: Date? 24 | private weak var browser: Browser? 25 | private var model: JSON? 26 | private var completion: StepCompletionCallback? 27 | 28 | /// Initializer. 29 | /// 30 | /// - parameter assertionName: Name of JavaScript function that evaluates the conditions and returns a Boolean. 31 | /// - parameter timeoutInSeconds: The number of seconds before the step fails due to timeout. 32 | public init(assertionName: String, timeoutInSeconds: TimeInterval) { 33 | self.assertionName = assertionName 34 | self.timeoutInSeconds = timeoutInSeconds 35 | } 36 | 37 | public func run(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) { 38 | startRunDate = Date() 39 | self.browser = browser 40 | self.model = model 41 | self.completion = completion 42 | timer = Timer.scheduledTimer(timeInterval: Constants.refreshInterval, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: true) 43 | } 44 | 45 | @objc func handleTimer() { 46 | guard let startRunDate = startRunDate, 47 | let browser = browser, 48 | let model = model, 49 | let completion = completion else { return } 50 | browser.runScript(functionName: assertionName) { [weak self] result in 51 | guard let this = self else { return } 52 | switch result { 53 | case .success(let ok): 54 | if ok as? Bool ?? false { 55 | this.reset() 56 | completion(.proceed(model)) 57 | } else { 58 | if Date().timeIntervalSince(startRunDate) > this.timeoutInSeconds { 59 | this.reset() 60 | completion(.failure(SwiftScraperError.timeout, model)) 61 | } 62 | } 63 | case .failure(let error): 64 | this.reset() 65 | completion(.failure(error, model)) 66 | } 67 | } 68 | } 69 | 70 | private func reset() { 71 | timer?.invalidate() 72 | timer = nil 73 | startRunDate = nil 74 | browser = nil 75 | model = nil 76 | completion = nil 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Steps/WaitStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaitStep.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Step that waits for a fixed number of seconds. 12 | public class WaitStep: Step { 13 | 14 | private var waitTimeInSeconds: TimeInterval 15 | 16 | /// Initializer. 17 | /// 18 | /// - parameter waitTimeInSeconds: The number of seconds to wait before proceeding to the next step 19 | public init(waitTimeInSeconds: TimeInterval) { 20 | self.waitTimeInSeconds = waitTimeInSeconds 21 | } 22 | 23 | public func run(with browser: Browser, model: JSON, completion: @escaping StepCompletionCallback) { 24 | DispatchQueue.main.asyncAfter(deadline: .now() + waitTimeInSeconds) { 25 | completion(.proceed(model)) 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Sources/SwiftScraper.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftScraper.h 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 20/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SwiftScraper. 12 | FOUNDATION_EXPORT double SwiftScraperVersionNumber; 13 | 14 | //! Project version string for SwiftScraper. 15 | FOUNDATION_EXPORT const unsigned char SwiftScraperVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/SwiftScraperError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftScraperError.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 21/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum SwiftScraperError: Error, LocalizedError { 12 | 13 | /// Problem with serializing parameters to pass to the JavaScript. 14 | case parameterSerialization 15 | 16 | /// An assertion failed, the page contents was not what was expected. 17 | case contentUnexpected 18 | 19 | /// JavaScript error occurred when trying to process the page. 20 | case javascriptError(errorMessage: String) 21 | 22 | /// Page navigation failed with the given error. 23 | case navigationFailed(error: Error) 24 | 25 | /// The step which was specified could not be found to be run, e.g. if an incorrect index was specified for `StepFlowResult.jumpToStep(Int)`. 26 | case incorrectStep 27 | 28 | /// Timeout occurred while waiting for a step to complete. 29 | case timeout 30 | 31 | public var errorDescription: String? { 32 | switch self { 33 | case .parameterSerialization: return "Could not serialize the parameters to pass to the script" 34 | case .contentUnexpected: return "Something went wrong, the page contents was not what was expected" 35 | case .javascriptError(let errorMessage): return "A JavaScript error occurred when trying to process the page: \(errorMessage)" 36 | case .navigationFailed: return "Something went wrong when navigating to the page" 37 | case .incorrectStep: return "An incorrect step was specified" 38 | case .timeout: return "Timeout occurred while waiting for a step to complete" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SwiftScraper.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SwiftScraper' 3 | s.version = '0.3.0' 4 | s.summary = 'Screen scraping orchestration for iOS in Swift.' 5 | s.description = <<-DESC 6 | Framework that makes it easy to integrate and orchestrate screen scraping with your Swift iOS app. 7 | DESC 8 | 9 | s.homepage = 'https://github.com/cweatureapps/SwiftScraper' 10 | s.license = { :type => 'MIT', :file => 'LICENSE' } 11 | s.author = { 'cweatureapps' => 'cweatureapps@gmail.com' } 12 | s.source = { :git => 'https://github.com/cweatureapps/SwiftScraper.git', :tag => s.version.to_s } 13 | s.resource_bundles = { "SwiftScraper" => ["Resources/**/*.{js}"] } 14 | s.ios.deployment_target = '8.0' 15 | s.source_files = 'Sources/**/*.{h,m,swift}' 16 | s.frameworks = 'UIKit', 'WebKit' 17 | s.dependency 'Observable-Swift', '~> 0.7' 18 | end 19 | -------------------------------------------------------------------------------- /SwiftScraper.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5B3B035A1EAF3DF80080617F /* page2.html in Resources */ = {isa = PBXBuildFile; fileRef = 5B3B03591EAF3DF70080617F /* page2.html */; }; 11 | 5B3B035D1EAF5AD90080617F /* SwiftScraper.js in Resources */ = {isa = PBXBuildFile; fileRef = 5B3B035C1EAF5AD90080617F /* SwiftScraper.js */; }; 12 | 5B4485471EB2123B008DF5B2 /* StepCompletionCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4485461EB2123B008DF5B2 /* StepCompletionCallback.swift */; }; 13 | 5B4485491EB212F9008DF5B2 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4485481EB212F9008DF5B2 /* JSON.swift */; }; 14 | 5B4660BE1EA87A6A005B921F /* SwiftScraper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B4660B41EA87A6A005B921F /* SwiftScraper.framework */; }; 15 | 5B4660C31EA87A6A005B921F /* StepRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4660C21EA87A6A005B921F /* StepRunnerTests.swift */; }; 16 | 5B4660C51EA87A6A005B921F /* SwiftScraper.h in Headers */ = {isa = PBXBuildFile; fileRef = 5B4660B71EA87A6A005B921F /* SwiftScraper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | 5B9F7AB21EB2AF2B00143CE4 /* waitTest.html in Resources */ = {isa = PBXBuildFile; fileRef = 5B9F7AB01EB2AF2600143CE4 /* waitTest.html */; }; 18 | 5B9F7AB41EB3214D00143CE4 /* WaitForConditionStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9F7AB31EB3214D00143CE4 /* WaitForConditionStep.swift */; }; 19 | 5B9F7AB61EB3718400143CE4 /* StepFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9F7AB51EB3718400143CE4 /* StepFlowResult.swift */; }; 20 | 5BC621391EB00BFE00EABB06 /* ProcessStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC621381EB00BFE00EABB06 /* ProcessStep.swift */; }; 21 | 5BC8791F1EAA1AD700230CEA /* Browser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC8791E1EAA1AD700230CEA /* Browser.swift */; }; 22 | 5BC879231EAA1B5800230CEA /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC879221EAA1B5800230CEA /* Result.swift */; }; 23 | 5BC879251EAA1B7600230CEA /* SwiftScraperError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC879241EAA1B7600230CEA /* SwiftScraperError.swift */; }; 24 | 5BC879281EAA1FE500230CEA /* Step.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC879271EAA1FE500230CEA /* Step.swift */; }; 25 | 5BC8792A1EAA1FFD00230CEA /* NavigableStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC879291EAA1FFD00230CEA /* NavigableStep.swift */; }; 26 | 5BC8792C1EAA201E00230CEA /* OpenPageStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC8792B1EAA201E00230CEA /* OpenPageStep.swift */; }; 27 | 5BC8792E1EAA203E00230CEA /* PageChangeStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC8792D1EAA203E00230CEA /* PageChangeStep.swift */; }; 28 | 5BC879301EAA211B00230CEA /* ScriptStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC8792F1EAA211B00230CEA /* ScriptStep.swift */; }; 29 | 5BC879321EAA213800230CEA /* AsyncScriptStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC879311EAA213800230CEA /* AsyncScriptStep.swift */; }; 30 | 5BC879341EAA21D700230CEA /* WaitStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC879331EAA21D700230CEA /* WaitStep.swift */; }; 31 | 5BC879361EAA224C00230CEA /* StepRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC879351EAA224C00230CEA /* StepRunner.swift */; }; 32 | 5BEF8AC81EAE01CD003E2945 /* page1.html in Resources */ = {isa = PBXBuildFile; fileRef = 5BEF8AC71EAE01CD003E2945 /* page1.html */; }; 33 | 5BEF8ACA1EAE069F003E2945 /* StepRunnerTests.js in Resources */ = {isa = PBXBuildFile; fileRef = 5BEF8AC91EAE069F003E2945 /* StepRunnerTests.js */; }; 34 | 5BEF8ACC1EAE122B003E2945 /* JavaScriptGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEF8ACB1EAE122B003E2945 /* JavaScriptGeneratorTests.swift */; }; 35 | 5BEF8ACF1EAE1A45003E2945 /* JavaScriptGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEF8ACD1EAE1982003E2945 /* JavaScriptGenerator.swift */; }; 36 | 6417A7A843E5211BA7365161 /* Pods_SwiftScraperTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC07E9FA0D7A637FB15E5C6A /* Pods_SwiftScraperTests.framework */; }; 37 | BAA5A332A3725AAAB96C8FF0 /* Pods_SwiftScraper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28010FCF547BEFA8CDA65CC3 /* Pods_SwiftScraper.framework */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXContainerItemProxy section */ 41 | 5B4660BF1EA87A6A005B921F /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = 5B4660AB1EA87A6A005B921F /* Project object */; 44 | proxyType = 1; 45 | remoteGlobalIDString = 5B4660B31EA87A6A005B921F; 46 | remoteInfo = SwiftScraper; 47 | }; 48 | /* End PBXContainerItemProxy section */ 49 | 50 | /* Begin PBXFileReference section */ 51 | 28010FCF547BEFA8CDA65CC3 /* Pods_SwiftScraper.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftScraper.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 2A1A7ADFE97773C67B0A9406 /* Pods-SwiftScraperTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftScraperTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests.debug.xcconfig"; sourceTree = ""; }; 53 | 546557DC044237AD5296319A /* Pods-SwiftScraper.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftScraper.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper.release.xcconfig"; sourceTree = ""; }; 54 | 5B3B03591EAF3DF70080617F /* page2.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page2.html; sourceTree = ""; }; 55 | 5B3B035C1EAF5AD90080617F /* SwiftScraper.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SwiftScraper.js; sourceTree = ""; }; 56 | 5B4485461EB2123B008DF5B2 /* StepCompletionCallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepCompletionCallback.swift; sourceTree = ""; }; 57 | 5B4485481EB212F9008DF5B2 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 58 | 5B4660B41EA87A6A005B921F /* SwiftScraper.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftScraper.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | 5B4660B71EA87A6A005B921F /* SwiftScraper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftScraper.h; sourceTree = ""; }; 60 | 5B4660B81EA87A6A005B921F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | 5B4660BD1EA87A6A005B921F /* SwiftScraperTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftScraperTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 5B4660C21EA87A6A005B921F /* StepRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepRunnerTests.swift; sourceTree = ""; }; 63 | 5B4660C41EA87A6A005B921F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 5B9F7AB01EB2AF2600143CE4 /* waitTest.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = waitTest.html; sourceTree = ""; }; 65 | 5B9F7AB31EB3214D00143CE4 /* WaitForConditionStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitForConditionStep.swift; sourceTree = ""; }; 66 | 5B9F7AB51EB3718400143CE4 /* StepFlowResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepFlowResult.swift; sourceTree = ""; }; 67 | 5BC621381EB00BFE00EABB06 /* ProcessStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessStep.swift; sourceTree = ""; }; 68 | 5BC8791E1EAA1AD700230CEA /* Browser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Browser.swift; sourceTree = ""; }; 69 | 5BC879221EAA1B5800230CEA /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 70 | 5BC879241EAA1B7600230CEA /* SwiftScraperError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftScraperError.swift; sourceTree = ""; }; 71 | 5BC879271EAA1FE500230CEA /* Step.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Step.swift; sourceTree = ""; }; 72 | 5BC879291EAA1FFD00230CEA /* NavigableStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigableStep.swift; sourceTree = ""; }; 73 | 5BC8792B1EAA201E00230CEA /* OpenPageStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenPageStep.swift; sourceTree = ""; }; 74 | 5BC8792D1EAA203E00230CEA /* PageChangeStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageChangeStep.swift; sourceTree = ""; }; 75 | 5BC8792F1EAA211B00230CEA /* ScriptStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScriptStep.swift; sourceTree = ""; }; 76 | 5BC879311EAA213800230CEA /* AsyncScriptStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncScriptStep.swift; sourceTree = ""; }; 77 | 5BC879331EAA21D700230CEA /* WaitStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitStep.swift; sourceTree = ""; }; 78 | 5BC879351EAA224C00230CEA /* StepRunner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepRunner.swift; sourceTree = ""; }; 79 | 5BEF8AC71EAE01CD003E2945 /* page1.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page1.html; sourceTree = ""; }; 80 | 5BEF8AC91EAE069F003E2945 /* StepRunnerTests.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = StepRunnerTests.js; sourceTree = ""; }; 81 | 5BEF8ACB1EAE122B003E2945 /* JavaScriptGeneratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JavaScriptGeneratorTests.swift; sourceTree = ""; }; 82 | 5BEF8ACD1EAE1982003E2945 /* JavaScriptGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JavaScriptGenerator.swift; sourceTree = ""; }; 83 | B7BAD8EE4A3E8AA4F5228DA5 /* Pods-SwiftScraper.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftScraper.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper.debug.xcconfig"; sourceTree = ""; }; 84 | BBD4C3740353A13CE16A8369 /* Pods-SwiftScraperTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftScraperTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests.release.xcconfig"; sourceTree = ""; }; 85 | DC07E9FA0D7A637FB15E5C6A /* Pods_SwiftScraperTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftScraperTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | 5B4660B01EA87A6A005B921F /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | BAA5A332A3725AAAB96C8FF0 /* Pods_SwiftScraper.framework in Frameworks */, 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | 5B4660BA1EA87A6A005B921F /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | 5B4660BE1EA87A6A005B921F /* SwiftScraper.framework in Frameworks */, 102 | 6417A7A843E5211BA7365161 /* Pods_SwiftScraperTests.framework in Frameworks */, 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | 5B3B035B1EAF59A00080617F /* Resources */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 5B3B035C1EAF5AD90080617F /* SwiftScraper.js */, 113 | ); 114 | path = Resources; 115 | sourceTree = ""; 116 | }; 117 | 5B4660AA1EA87A6A005B921F = { 118 | isa = PBXGroup; 119 | children = ( 120 | 92685D469D3B000D8A2BEE2B /* Frameworks */, 121 | B949B45A874E06C76BF2DA45 /* Pods */, 122 | 5B4660B51EA87A6A005B921F /* Products */, 123 | 5B3B035B1EAF59A00080617F /* Resources */, 124 | 5B4660B61EA87A6A005B921F /* Sources */, 125 | 5B4660C11EA87A6A005B921F /* Tests */, 126 | ); 127 | sourceTree = ""; 128 | }; 129 | 5B4660B51EA87A6A005B921F /* Products */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 5B4660B41EA87A6A005B921F /* SwiftScraper.framework */, 133 | 5B4660BD1EA87A6A005B921F /* SwiftScraperTests.xctest */, 134 | ); 135 | name = Products; 136 | sourceTree = ""; 137 | }; 138 | 5B4660B61EA87A6A005B921F /* Sources */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 5BC8791E1EAA1AD700230CEA /* Browser.swift */, 142 | 5B4660B81EA87A6A005B921F /* Info.plist */, 143 | 5BEF8ACD1EAE1982003E2945 /* JavaScriptGenerator.swift */, 144 | 5B4485481EB212F9008DF5B2 /* JSON.swift */, 145 | 5BC879221EAA1B5800230CEA /* Result.swift */, 146 | 5BC879351EAA224C00230CEA /* StepRunner.swift */, 147 | 5BC879261EAA1FD100230CEA /* Steps */, 148 | 5B4660B71EA87A6A005B921F /* SwiftScraper.h */, 149 | 5BC879241EAA1B7600230CEA /* SwiftScraperError.swift */, 150 | ); 151 | path = Sources; 152 | sourceTree = ""; 153 | }; 154 | 5B4660C11EA87A6A005B921F /* Tests */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 5B4660C41EA87A6A005B921F /* Info.plist */, 158 | 5BEF8ACB1EAE122B003E2945 /* JavaScriptGeneratorTests.swift */, 159 | 5BEF8AC61EAE01B4003E2945 /* Pages */, 160 | 5BEF8AC91EAE069F003E2945 /* StepRunnerTests.js */, 161 | 5B4660C21EA87A6A005B921F /* StepRunnerTests.swift */, 162 | ); 163 | path = Tests; 164 | sourceTree = ""; 165 | }; 166 | 5BC879261EAA1FD100230CEA /* Steps */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 5BC879311EAA213800230CEA /* AsyncScriptStep.swift */, 170 | 5BC879291EAA1FFD00230CEA /* NavigableStep.swift */, 171 | 5BC8792B1EAA201E00230CEA /* OpenPageStep.swift */, 172 | 5BC8792D1EAA203E00230CEA /* PageChangeStep.swift */, 173 | 5BC621381EB00BFE00EABB06 /* ProcessStep.swift */, 174 | 5BC8792F1EAA211B00230CEA /* ScriptStep.swift */, 175 | 5BC879271EAA1FE500230CEA /* Step.swift */, 176 | 5B4485461EB2123B008DF5B2 /* StepCompletionCallback.swift */, 177 | 5B9F7AB51EB3718400143CE4 /* StepFlowResult.swift */, 178 | 5B9F7AB31EB3214D00143CE4 /* WaitForConditionStep.swift */, 179 | 5BC879331EAA21D700230CEA /* WaitStep.swift */, 180 | ); 181 | path = Steps; 182 | sourceTree = ""; 183 | }; 184 | 5BEF8AC61EAE01B4003E2945 /* Pages */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 5BEF8AC71EAE01CD003E2945 /* page1.html */, 188 | 5B3B03591EAF3DF70080617F /* page2.html */, 189 | 5B9F7AB01EB2AF2600143CE4 /* waitTest.html */, 190 | ); 191 | path = Pages; 192 | sourceTree = ""; 193 | }; 194 | 92685D469D3B000D8A2BEE2B /* Frameworks */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | 28010FCF547BEFA8CDA65CC3 /* Pods_SwiftScraper.framework */, 198 | DC07E9FA0D7A637FB15E5C6A /* Pods_SwiftScraperTests.framework */, 199 | ); 200 | name = Frameworks; 201 | sourceTree = ""; 202 | }; 203 | B949B45A874E06C76BF2DA45 /* Pods */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | B7BAD8EE4A3E8AA4F5228DA5 /* Pods-SwiftScraper.debug.xcconfig */, 207 | 546557DC044237AD5296319A /* Pods-SwiftScraper.release.xcconfig */, 208 | 2A1A7ADFE97773C67B0A9406 /* Pods-SwiftScraperTests.debug.xcconfig */, 209 | BBD4C3740353A13CE16A8369 /* Pods-SwiftScraperTests.release.xcconfig */, 210 | ); 211 | name = Pods; 212 | sourceTree = ""; 213 | }; 214 | /* End PBXGroup section */ 215 | 216 | /* Begin PBXHeadersBuildPhase section */ 217 | 5B4660B11EA87A6A005B921F /* Headers */ = { 218 | isa = PBXHeadersBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | 5B4660C51EA87A6A005B921F /* SwiftScraper.h in Headers */, 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | /* End PBXHeadersBuildPhase section */ 226 | 227 | /* Begin PBXNativeTarget section */ 228 | 5B4660B31EA87A6A005B921F /* SwiftScraper */ = { 229 | isa = PBXNativeTarget; 230 | buildConfigurationList = 5B4660C81EA87A6A005B921F /* Build configuration list for PBXNativeTarget "SwiftScraper" */; 231 | buildPhases = ( 232 | 5DD990F1037A607A198B7913 /* [CP] Check Pods Manifest.lock */, 233 | 5B4660AF1EA87A6A005B921F /* Sources */, 234 | 5B4660B01EA87A6A005B921F /* Frameworks */, 235 | 5B4660B11EA87A6A005B921F /* Headers */, 236 | 5B4660B21EA87A6A005B921F /* Resources */, 237 | 73F0E93CD38A7A1441691AC7 /* [CP] Copy Pods Resources */, 238 | ); 239 | buildRules = ( 240 | ); 241 | dependencies = ( 242 | ); 243 | name = SwiftScraper; 244 | productName = SwiftScraper; 245 | productReference = 5B4660B41EA87A6A005B921F /* SwiftScraper.framework */; 246 | productType = "com.apple.product-type.framework"; 247 | }; 248 | 5B4660BC1EA87A6A005B921F /* SwiftScraperTests */ = { 249 | isa = PBXNativeTarget; 250 | buildConfigurationList = 5B4660CB1EA87A6A005B921F /* Build configuration list for PBXNativeTarget "SwiftScraperTests" */; 251 | buildPhases = ( 252 | F27F4F108CD7987F4433C830 /* [CP] Check Pods Manifest.lock */, 253 | 5B4660B91EA87A6A005B921F /* Sources */, 254 | 5B4660BA1EA87A6A005B921F /* Frameworks */, 255 | 5B4660BB1EA87A6A005B921F /* Resources */, 256 | 0FAABA1089B0D9B41A39F325 /* [CP] Embed Pods Frameworks */, 257 | 5E3831BE680739AF1A4DE360 /* [CP] Copy Pods Resources */, 258 | ); 259 | buildRules = ( 260 | ); 261 | dependencies = ( 262 | 5B4660C01EA87A6A005B921F /* PBXTargetDependency */, 263 | ); 264 | name = SwiftScraperTests; 265 | productName = SwiftScraperTests; 266 | productReference = 5B4660BD1EA87A6A005B921F /* SwiftScraperTests.xctest */; 267 | productType = "com.apple.product-type.bundle.unit-test"; 268 | }; 269 | /* End PBXNativeTarget section */ 270 | 271 | /* Begin PBXProject section */ 272 | 5B4660AB1EA87A6A005B921F /* Project object */ = { 273 | isa = PBXProject; 274 | attributes = { 275 | LastSwiftUpdateCheck = 0820; 276 | LastUpgradeCheck = 0820; 277 | ORGANIZATIONNAME = "Ken Ko"; 278 | TargetAttributes = { 279 | 5B4660B31EA87A6A005B921F = { 280 | CreatedOnToolsVersion = 8.2.1; 281 | LastSwiftMigration = 0820; 282 | ProvisioningStyle = Automatic; 283 | }; 284 | 5B4660BC1EA87A6A005B921F = { 285 | CreatedOnToolsVersion = 8.2.1; 286 | ProvisioningStyle = Automatic; 287 | }; 288 | }; 289 | }; 290 | buildConfigurationList = 5B4660AE1EA87A6A005B921F /* Build configuration list for PBXProject "SwiftScraper" */; 291 | compatibilityVersion = "Xcode 3.2"; 292 | developmentRegion = English; 293 | hasScannedForEncodings = 0; 294 | knownRegions = ( 295 | en, 296 | ); 297 | mainGroup = 5B4660AA1EA87A6A005B921F; 298 | productRefGroup = 5B4660B51EA87A6A005B921F /* Products */; 299 | projectDirPath = ""; 300 | projectRoot = ""; 301 | targets = ( 302 | 5B4660B31EA87A6A005B921F /* SwiftScraper */, 303 | 5B4660BC1EA87A6A005B921F /* SwiftScraperTests */, 304 | ); 305 | }; 306 | /* End PBXProject section */ 307 | 308 | /* Begin PBXResourcesBuildPhase section */ 309 | 5B4660B21EA87A6A005B921F /* Resources */ = { 310 | isa = PBXResourcesBuildPhase; 311 | buildActionMask = 2147483647; 312 | files = ( 313 | 5B3B035D1EAF5AD90080617F /* SwiftScraper.js in Resources */, 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | }; 317 | 5B4660BB1EA87A6A005B921F /* Resources */ = { 318 | isa = PBXResourcesBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | 5B3B035A1EAF3DF80080617F /* page2.html in Resources */, 322 | 5B9F7AB21EB2AF2B00143CE4 /* waitTest.html in Resources */, 323 | 5BEF8AC81EAE01CD003E2945 /* page1.html in Resources */, 324 | 5BEF8ACA1EAE069F003E2945 /* StepRunnerTests.js in Resources */, 325 | ); 326 | runOnlyForDeploymentPostprocessing = 0; 327 | }; 328 | /* End PBXResourcesBuildPhase section */ 329 | 330 | /* Begin PBXShellScriptBuildPhase section */ 331 | 0FAABA1089B0D9B41A39F325 /* [CP] Embed Pods Frameworks */ = { 332 | isa = PBXShellScriptBuildPhase; 333 | buildActionMask = 2147483647; 334 | files = ( 335 | ); 336 | inputPaths = ( 337 | ); 338 | name = "[CP] Embed Pods Frameworks"; 339 | outputPaths = ( 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | shellPath = /bin/sh; 343 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-frameworks.sh\"\n"; 344 | showEnvVarsInLog = 0; 345 | }; 346 | 5DD990F1037A607A198B7913 /* [CP] Check Pods Manifest.lock */ = { 347 | isa = PBXShellScriptBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | ); 351 | inputPaths = ( 352 | ); 353 | name = "[CP] Check Pods Manifest.lock"; 354 | outputPaths = ( 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | shellPath = /bin/sh; 358 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 359 | showEnvVarsInLog = 0; 360 | }; 361 | 5E3831BE680739AF1A4DE360 /* [CP] Copy Pods Resources */ = { 362 | isa = PBXShellScriptBuildPhase; 363 | buildActionMask = 2147483647; 364 | files = ( 365 | ); 366 | inputPaths = ( 367 | ); 368 | name = "[CP] Copy Pods Resources"; 369 | outputPaths = ( 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | shellPath = /bin/sh; 373 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftScraperTests/Pods-SwiftScraperTests-resources.sh\"\n"; 374 | showEnvVarsInLog = 0; 375 | }; 376 | 73F0E93CD38A7A1441691AC7 /* [CP] Copy Pods Resources */ = { 377 | isa = PBXShellScriptBuildPhase; 378 | buildActionMask = 2147483647; 379 | files = ( 380 | ); 381 | inputPaths = ( 382 | ); 383 | name = "[CP] Copy Pods Resources"; 384 | outputPaths = ( 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | shellPath = /bin/sh; 388 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftScraper/Pods-SwiftScraper-resources.sh\"\n"; 389 | showEnvVarsInLog = 0; 390 | }; 391 | F27F4F108CD7987F4433C830 /* [CP] Check Pods Manifest.lock */ = { 392 | isa = PBXShellScriptBuildPhase; 393 | buildActionMask = 2147483647; 394 | files = ( 395 | ); 396 | inputPaths = ( 397 | ); 398 | name = "[CP] Check Pods Manifest.lock"; 399 | outputPaths = ( 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | shellPath = /bin/sh; 403 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 404 | showEnvVarsInLog = 0; 405 | }; 406 | /* End PBXShellScriptBuildPhase section */ 407 | 408 | /* Begin PBXSourcesBuildPhase section */ 409 | 5B4660AF1EA87A6A005B921F /* Sources */ = { 410 | isa = PBXSourcesBuildPhase; 411 | buildActionMask = 2147483647; 412 | files = ( 413 | 5BC879231EAA1B5800230CEA /* Result.swift in Sources */, 414 | 5BC621391EB00BFE00EABB06 /* ProcessStep.swift in Sources */, 415 | 5BC879361EAA224C00230CEA /* StepRunner.swift in Sources */, 416 | 5BC879341EAA21D700230CEA /* WaitStep.swift in Sources */, 417 | 5B9F7AB41EB3214D00143CE4 /* WaitForConditionStep.swift in Sources */, 418 | 5BC8792A1EAA1FFD00230CEA /* NavigableStep.swift in Sources */, 419 | 5BEF8ACF1EAE1A45003E2945 /* JavaScriptGenerator.swift in Sources */, 420 | 5B9F7AB61EB3718400143CE4 /* StepFlowResult.swift in Sources */, 421 | 5BC8792C1EAA201E00230CEA /* OpenPageStep.swift in Sources */, 422 | 5BC8791F1EAA1AD700230CEA /* Browser.swift in Sources */, 423 | 5BC879281EAA1FE500230CEA /* Step.swift in Sources */, 424 | 5B4485471EB2123B008DF5B2 /* StepCompletionCallback.swift in Sources */, 425 | 5BC879301EAA211B00230CEA /* ScriptStep.swift in Sources */, 426 | 5BC879321EAA213800230CEA /* AsyncScriptStep.swift in Sources */, 427 | 5BC8792E1EAA203E00230CEA /* PageChangeStep.swift in Sources */, 428 | 5BC879251EAA1B7600230CEA /* SwiftScraperError.swift in Sources */, 429 | 5B4485491EB212F9008DF5B2 /* JSON.swift in Sources */, 430 | ); 431 | runOnlyForDeploymentPostprocessing = 0; 432 | }; 433 | 5B4660B91EA87A6A005B921F /* Sources */ = { 434 | isa = PBXSourcesBuildPhase; 435 | buildActionMask = 2147483647; 436 | files = ( 437 | 5BEF8ACC1EAE122B003E2945 /* JavaScriptGeneratorTests.swift in Sources */, 438 | 5B4660C31EA87A6A005B921F /* StepRunnerTests.swift in Sources */, 439 | ); 440 | runOnlyForDeploymentPostprocessing = 0; 441 | }; 442 | /* End PBXSourcesBuildPhase section */ 443 | 444 | /* Begin PBXTargetDependency section */ 445 | 5B4660C01EA87A6A005B921F /* PBXTargetDependency */ = { 446 | isa = PBXTargetDependency; 447 | target = 5B4660B31EA87A6A005B921F /* SwiftScraper */; 448 | targetProxy = 5B4660BF1EA87A6A005B921F /* PBXContainerItemProxy */; 449 | }; 450 | /* End PBXTargetDependency section */ 451 | 452 | /* Begin XCBuildConfiguration section */ 453 | 5B4660C61EA87A6A005B921F /* Debug */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ALWAYS_SEARCH_USER_PATHS = NO; 457 | CLANG_ANALYZER_NONNULL = YES; 458 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 459 | CLANG_CXX_LIBRARY = "libc++"; 460 | CLANG_ENABLE_MODULES = YES; 461 | CLANG_ENABLE_OBJC_ARC = YES; 462 | CLANG_WARN_BOOL_CONVERSION = YES; 463 | CLANG_WARN_CONSTANT_CONVERSION = YES; 464 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 465 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 466 | CLANG_WARN_EMPTY_BODY = YES; 467 | CLANG_WARN_ENUM_CONVERSION = YES; 468 | CLANG_WARN_INFINITE_RECURSION = YES; 469 | CLANG_WARN_INT_CONVERSION = YES; 470 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 471 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 472 | CLANG_WARN_UNREACHABLE_CODE = YES; 473 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 474 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 475 | COPY_PHASE_STRIP = NO; 476 | CURRENT_PROJECT_VERSION = 1; 477 | DEBUG_INFORMATION_FORMAT = dwarf; 478 | ENABLE_STRICT_OBJC_MSGSEND = YES; 479 | ENABLE_TESTABILITY = YES; 480 | GCC_C_LANGUAGE_STANDARD = gnu99; 481 | GCC_DYNAMIC_NO_PIC = NO; 482 | GCC_NO_COMMON_BLOCKS = YES; 483 | GCC_OPTIMIZATION_LEVEL = 0; 484 | GCC_PREPROCESSOR_DEFINITIONS = ( 485 | "DEBUG=1", 486 | "$(inherited)", 487 | ); 488 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 489 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 490 | GCC_WARN_UNDECLARED_SELECTOR = YES; 491 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 492 | GCC_WARN_UNUSED_FUNCTION = YES; 493 | GCC_WARN_UNUSED_VARIABLE = YES; 494 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 495 | MTL_ENABLE_DEBUG_INFO = YES; 496 | ONLY_ACTIVE_ARCH = YES; 497 | SDKROOT = iphoneos; 498 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 499 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 500 | SWIFT_VERSION = 4.0; 501 | TARGETED_DEVICE_FAMILY = "1,2"; 502 | VERSIONING_SYSTEM = "apple-generic"; 503 | VERSION_INFO_PREFIX = ""; 504 | }; 505 | name = Debug; 506 | }; 507 | 5B4660C71EA87A6A005B921F /* Release */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | ALWAYS_SEARCH_USER_PATHS = NO; 511 | CLANG_ANALYZER_NONNULL = YES; 512 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 513 | CLANG_CXX_LIBRARY = "libc++"; 514 | CLANG_ENABLE_MODULES = YES; 515 | CLANG_ENABLE_OBJC_ARC = YES; 516 | CLANG_WARN_BOOL_CONVERSION = YES; 517 | CLANG_WARN_CONSTANT_CONVERSION = YES; 518 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 519 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 520 | CLANG_WARN_EMPTY_BODY = YES; 521 | CLANG_WARN_ENUM_CONVERSION = YES; 522 | CLANG_WARN_INFINITE_RECURSION = YES; 523 | CLANG_WARN_INT_CONVERSION = YES; 524 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 525 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 526 | CLANG_WARN_UNREACHABLE_CODE = YES; 527 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 528 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 529 | COPY_PHASE_STRIP = NO; 530 | CURRENT_PROJECT_VERSION = 1; 531 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 532 | ENABLE_NS_ASSERTIONS = NO; 533 | ENABLE_STRICT_OBJC_MSGSEND = YES; 534 | GCC_C_LANGUAGE_STANDARD = gnu99; 535 | GCC_NO_COMMON_BLOCKS = YES; 536 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 537 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 538 | GCC_WARN_UNDECLARED_SELECTOR = YES; 539 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 540 | GCC_WARN_UNUSED_FUNCTION = YES; 541 | GCC_WARN_UNUSED_VARIABLE = YES; 542 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 543 | MTL_ENABLE_DEBUG_INFO = NO; 544 | SDKROOT = iphoneos; 545 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 546 | SWIFT_VERSION = 4.0; 547 | TARGETED_DEVICE_FAMILY = "1,2"; 548 | VALIDATE_PRODUCT = YES; 549 | VERSIONING_SYSTEM = "apple-generic"; 550 | VERSION_INFO_PREFIX = ""; 551 | }; 552 | name = Release; 553 | }; 554 | 5B4660C91EA87A6A005B921F /* Debug */ = { 555 | isa = XCBuildConfiguration; 556 | baseConfigurationReference = B7BAD8EE4A3E8AA4F5228DA5 /* Pods-SwiftScraper.debug.xcconfig */; 557 | buildSettings = { 558 | CLANG_ENABLE_MODULES = YES; 559 | CODE_SIGN_IDENTITY = ""; 560 | DEFINES_MODULE = YES; 561 | DYLIB_COMPATIBILITY_VERSION = 1; 562 | DYLIB_CURRENT_VERSION = 1; 563 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 564 | INFOPLIST_FILE = Sources/Info.plist; 565 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 566 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 567 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 568 | PRODUCT_BUNDLE_IDENTIFIER = com.cweatureapps.SwiftScraper; 569 | PRODUCT_NAME = "$(TARGET_NAME)"; 570 | SKIP_INSTALL = YES; 571 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 572 | }; 573 | name = Debug; 574 | }; 575 | 5B4660CA1EA87A6A005B921F /* Release */ = { 576 | isa = XCBuildConfiguration; 577 | baseConfigurationReference = 546557DC044237AD5296319A /* Pods-SwiftScraper.release.xcconfig */; 578 | buildSettings = { 579 | CLANG_ENABLE_MODULES = YES; 580 | CODE_SIGN_IDENTITY = ""; 581 | DEFINES_MODULE = YES; 582 | DYLIB_COMPATIBILITY_VERSION = 1; 583 | DYLIB_CURRENT_VERSION = 1; 584 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 585 | INFOPLIST_FILE = Sources/Info.plist; 586 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 587 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 588 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 589 | PRODUCT_BUNDLE_IDENTIFIER = com.cweatureapps.SwiftScraper; 590 | PRODUCT_NAME = "$(TARGET_NAME)"; 591 | SKIP_INSTALL = YES; 592 | }; 593 | name = Release; 594 | }; 595 | 5B4660CC1EA87A6A005B921F /* Debug */ = { 596 | isa = XCBuildConfiguration; 597 | baseConfigurationReference = 2A1A7ADFE97773C67B0A9406 /* Pods-SwiftScraperTests.debug.xcconfig */; 598 | buildSettings = { 599 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 600 | INFOPLIST_FILE = Tests/Info.plist; 601 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 602 | PRODUCT_BUNDLE_IDENTIFIER = com.cweatureapps.SwiftScraperTests; 603 | PRODUCT_NAME = "$(TARGET_NAME)"; 604 | }; 605 | name = Debug; 606 | }; 607 | 5B4660CD1EA87A6A005B921F /* Release */ = { 608 | isa = XCBuildConfiguration; 609 | baseConfigurationReference = BBD4C3740353A13CE16A8369 /* Pods-SwiftScraperTests.release.xcconfig */; 610 | buildSettings = { 611 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 612 | INFOPLIST_FILE = Tests/Info.plist; 613 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 614 | PRODUCT_BUNDLE_IDENTIFIER = com.cweatureapps.SwiftScraperTests; 615 | PRODUCT_NAME = "$(TARGET_NAME)"; 616 | }; 617 | name = Release; 618 | }; 619 | /* End XCBuildConfiguration section */ 620 | 621 | /* Begin XCConfigurationList section */ 622 | 5B4660AE1EA87A6A005B921F /* Build configuration list for PBXProject "SwiftScraper" */ = { 623 | isa = XCConfigurationList; 624 | buildConfigurations = ( 625 | 5B4660C61EA87A6A005B921F /* Debug */, 626 | 5B4660C71EA87A6A005B921F /* Release */, 627 | ); 628 | defaultConfigurationIsVisible = 0; 629 | defaultConfigurationName = Release; 630 | }; 631 | 5B4660C81EA87A6A005B921F /* Build configuration list for PBXNativeTarget "SwiftScraper" */ = { 632 | isa = XCConfigurationList; 633 | buildConfigurations = ( 634 | 5B4660C91EA87A6A005B921F /* Debug */, 635 | 5B4660CA1EA87A6A005B921F /* Release */, 636 | ); 637 | defaultConfigurationIsVisible = 0; 638 | defaultConfigurationName = Release; 639 | }; 640 | 5B4660CB1EA87A6A005B921F /* Build configuration list for PBXNativeTarget "SwiftScraperTests" */ = { 641 | isa = XCConfigurationList; 642 | buildConfigurations = ( 643 | 5B4660CC1EA87A6A005B921F /* Debug */, 644 | 5B4660CD1EA87A6A005B921F /* Release */, 645 | ); 646 | defaultConfigurationIsVisible = 0; 647 | defaultConfigurationName = Release; 648 | }; 649 | /* End XCConfigurationList section */ 650 | }; 651 | rootObject = 5B4660AB1EA87A6A005B921F /* Project object */; 652 | } 653 | -------------------------------------------------------------------------------- /SwiftScraper.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftScraper.xcodeproj/xcshareddata/xcschemes/SwiftScraper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /SwiftScraper.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/JavaScriptGeneratorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowserTests.swift 3 | // SwiftScraper 4 | // 5 | // Created by Ken Ko on 24/04/2017. 6 | // Copyright © 2017 Ken Ko. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftScraper 11 | 12 | class JavaScriptGeneratorTests: XCTestCase { 13 | 14 | func testGenerateScriptNoArg() { 15 | let script = try? JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething") 16 | XCTAssertEqual(script, "MyModule.doSomething()") 17 | } 18 | 19 | func testGenerateNullArg() { 20 | let script = try? JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething", params: [NSNull()]) 21 | XCTAssertEqual(script, "MyModule.doSomething(null)") 22 | } 23 | 24 | func testGenerateScriptStringArg() { 25 | let script = try? JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething", params: ["hello"]) 26 | XCTAssertEqual(script, "MyModule.doSomething(\"hello\")") 27 | } 28 | 29 | func testGenerateScriptNumericArg() { 30 | let script1 = try? JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething", params: [3]) 31 | XCTAssertEqual(script1, "MyModule.doSomething(3)") 32 | 33 | let script2 = try? JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething", params: [75.26]) 34 | XCTAssertEqual(script2, "MyModule.doSomething(75.26)") 35 | } 36 | 37 | func testGenerateScriptArrayArg() { 38 | let script1 = try? JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething", params: [[1,2,3]]) 39 | XCTAssertEqual(script1, "MyModule.doSomething([1,2,3])") 40 | 41 | let script2 = try? JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething", params: [["a", "b"]]) 42 | XCTAssertEqual(script2, "MyModule.doSomething([\"a\",\"b\"])") 43 | } 44 | 45 | func testGenerateMultipleArgs() { 46 | let script1 = try? JavaScriptGenerator.generateScript( 47 | moduleName: "MyModule", 48 | functionName: "doSomething", 49 | params: ["lorem", 45, 0.544]) 50 | XCTAssertEqual(script1, "MyModule.doSomething(\"lorem\",45,0.544)") 51 | 52 | let script2 = try? JavaScriptGenerator.generateScript( 53 | moduleName: "MyModule", 54 | functionName: "doSomething", 55 | params: ["lorem", NSNull(), ["message": "foo"]]) 56 | XCTAssertEqual(script2, "MyModule.doSomething(\"lorem\",null,{\"message\":\"foo\"})") 57 | } 58 | 59 | func testGenerateScriptJSONArg() { 60 | let json: JSON = [ 61 | "someString": "lorem ipsum", 62 | "someInt": 3, 63 | "someDouble": 5.6, 64 | "someBool": true, 65 | "someArray": [1,2,3], 66 | "someObject": [ 67 | "message": "hello world!" 68 | ] 69 | ] 70 | let script = try! JavaScriptGenerator.generateScript(moduleName: "MyModule", functionName: "doSomething", params: [json]) 71 | XCTAssertTrue(script.hasPrefix("MyModule.doSomething(")) 72 | XCTAssertTrue(script.hasSuffix(")")) 73 | 74 | // convert the parameter back into Swift JSON to assert 75 | let paramString = script.replacingOccurrences(of: "MyModule.doSomething(", with: "") 76 | .replacingOccurrences(of: ")", with: "") 77 | let jsonData = paramString.data(using: String.Encoding.utf8)! 78 | let jsonObject = try! JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as! JSON 79 | XCTAssertEqual(jsonObject["someString"] as! String, "lorem ipsum") 80 | XCTAssertEqual(jsonObject["someInt"] as! Int, 3) 81 | XCTAssertEqual(jsonObject["someDouble"] as! Double, 5.6) 82 | XCTAssertEqual(jsonObject["someBool"] as! Bool, true) 83 | XCTAssertEqual(jsonObject["someArray"] as! [Int], [1,2,3]) 84 | 85 | let innerObject = jsonObject["someObject"] as! JSON 86 | XCTAssertEqual(innerObject["message"] as! String, "hello world!") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/Pages/page1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 1 5 | 6 | 7 | 8 |

Hello world!

9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/Pages/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 2 5 | 6 | 7 |

This is the second page

8 | something 9 | 10 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/Pages/waitTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wait Test 5 | 6 | 7 | something 8 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Tests/StepRunnerTests.js: -------------------------------------------------------------------------------- 1 | var StepRunnerTests = (function() { 2 | function assertPage1Title() { 3 | return document.title == 'Page 1'; 4 | } 5 | 6 | function assertPage2Title() { 7 | return document.title == 'Page 2'; 8 | } 9 | 10 | function assertWaitTestTitle() { 11 | return document.title == 'Wait Test'; 12 | } 13 | 14 | function getInnerText(selector) { 15 | return document.querySelector(selector).innerText; 16 | } 17 | 18 | function getString() { 19 | return 'hello world'; 20 | } 21 | 22 | function getBooleanTrue(b) { 23 | return true; 24 | } 25 | 26 | function getBooleanFalse(b) { 27 | return false; 28 | } 29 | 30 | function getNumber() { 31 | return 3.45; 32 | } 33 | 34 | function getJsonObject() { 35 | return {message: 'something'}; 36 | } 37 | 38 | function getJsonArray() { 39 | return [{ fruit: 'apple' }, { fruit: 'pear' }]; 40 | } 41 | 42 | function multiArg(n, b, s, numArr, obj) { 43 | return { 44 | number: n, 45 | bool: b, 46 | text: s, 47 | numArr: numArr, 48 | obj: obj 49 | }; 50 | } 51 | 52 | function testParamsFromModel(text, number, nullVariable, obj) { 53 | return text === "hello world" && number === 987.6 && nullVariable === null && obj["foo"] === "bar"; 54 | } 55 | 56 | function goToPage2() { 57 | window.location = "page2.html" 58 | } 59 | 60 | function goToPage2WithParams(fruit, color) { 61 | window.location = 'page2.html?fruit=' + fruit + '&color=' + color; 62 | } 63 | 64 | function getStringAsync() { 65 | setTimeout(function() { 66 | SwiftScraper.postMessage('thanks for waiting...hello!'); 67 | }, 200); 68 | } 69 | 70 | function multiArgAsync(n, b, s, numArr, obj) { 71 | SwiftScraper.postMessage({ 72 | number: n, 73 | bool: b, 74 | text: s, 75 | numArr: numArr, 76 | obj: obj 77 | }); 78 | } 79 | 80 | function testWaitForCondition() { 81 | return document.getElementById('foo').innerText == 'modified'; 82 | } 83 | 84 | function generateException() { 85 | throw "JavaScript exception thrown"; 86 | } 87 | 88 | function modifyPage1Heading(text) { 89 | document.querySelector('h1').innerText = text; 90 | } 91 | 92 | return { 93 | assertPage1Title: assertPage1Title, 94 | assertPage2Title: assertPage2Title, 95 | assertWaitTestTitle: assertWaitTestTitle, 96 | getInnerText: getInnerText, 97 | getString: getString, 98 | getBooleanTrue: getBooleanTrue, 99 | getBooleanFalse: getBooleanFalse, 100 | getNumber: getNumber, 101 | getJsonObject: getJsonObject, 102 | getJsonArray: getJsonArray, 103 | multiArg: multiArg, 104 | testParamsFromModel: testParamsFromModel, 105 | goToPage2: goToPage2, 106 | goToPage2WithParams: goToPage2WithParams, 107 | getStringAsync: getStringAsync, 108 | multiArgAsync: multiArgAsync, 109 | testWaitForCondition: testWaitForCondition, 110 | generateException: generateException, 111 | modifyPage1Heading: modifyPage1Heading 112 | }; 113 | })(); 114 | --------------------------------------------------------------------------------