├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── ViewController.swift ├── LICENSE ├── README.md ├── WebViewContentPositioner.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── WebViewContentPositioner ├── Info.plist ├── UIWebView+WebViewContentPositioner.m ├── UIWebView+WebViewContentPositioner.swift ├── WebViewContentPositioner.h └── positioner.js └── WebViewContentPositionerTests ├── Info.plist └── WebViewContentPositionerTests.swift /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E0B07DC31C3E5230001CCAB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B07DC21C3E5230001CCAB7 /* AppDelegate.swift */; }; 11 | E0B07DC51C3E5230001CCAB7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B07DC41C3E5230001CCAB7 /* ViewController.swift */; }; 12 | E0B07DCA1C3E5230001CCAB7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E0B07DC91C3E5230001CCAB7 /* Assets.xcassets */; }; 13 | E0B07DCD1C3E5230001CCAB7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E0B07DCB1C3E5230001CCAB7 /* LaunchScreen.storyboard */; }; 14 | E0B07DDF1C3E52DA001CCAB7 /* WebViewContentPositioner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0B07DDC1C3E52D0001CCAB7 /* WebViewContentPositioner.framework */; }; 15 | E0B07DE01C3E52DA001CCAB7 /* WebViewContentPositioner.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E0B07DDC1C3E52D0001CCAB7 /* WebViewContentPositioner.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXContainerItemProxy section */ 19 | E0B07DDB1C3E52D0001CCAB7 /* PBXContainerItemProxy */ = { 20 | isa = PBXContainerItemProxy; 21 | containerPortal = E0B07DD61C3E52D0001CCAB7 /* WebViewContentPositioner.xcodeproj */; 22 | proxyType = 2; 23 | remoteGlobalIDString = E0B07D681C3D40C1001CCAB7; 24 | remoteInfo = WebViewContentPositioner; 25 | }; 26 | E0B07DDD1C3E52D0001CCAB7 /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = E0B07DD61C3E52D0001CCAB7 /* WebViewContentPositioner.xcodeproj */; 29 | proxyType = 2; 30 | remoteGlobalIDString = E0B07D721C3D40C1001CCAB7; 31 | remoteInfo = WebViewContentPositionerTests; 32 | }; 33 | E0B07DE11C3E52DA001CCAB7 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = E0B07DD61C3E52D0001CCAB7 /* WebViewContentPositioner.xcodeproj */; 36 | proxyType = 1; 37 | remoteGlobalIDString = E0B07D671C3D40C1001CCAB7; 38 | remoteInfo = WebViewContentPositioner; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXCopyFilesBuildPhase section */ 43 | E0B07DE31C3E52DA001CCAB7 /* Embed Frameworks */ = { 44 | isa = PBXCopyFilesBuildPhase; 45 | buildActionMask = 2147483647; 46 | dstPath = ""; 47 | dstSubfolderSpec = 10; 48 | files = ( 49 | E0B07DE01C3E52DA001CCAB7 /* WebViewContentPositioner.framework in Embed Frameworks */, 50 | ); 51 | name = "Embed Frameworks"; 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXCopyFilesBuildPhase section */ 55 | 56 | /* Begin PBXFileReference section */ 57 | E0B07DBF1C3E5230001CCAB7 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | E0B07DC21C3E5230001CCAB7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | E0B07DC41C3E5230001CCAB7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 60 | E0B07DC91C3E5230001CCAB7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61 | E0B07DCC1C3E5230001CCAB7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 62 | E0B07DCE1C3E5230001CCAB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63 | E0B07DD61C3E52D0001CCAB7 /* WebViewContentPositioner.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = WebViewContentPositioner.xcodeproj; path = ../WebViewContentPositioner.xcodeproj; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | E0B07DBC1C3E5230001CCAB7 /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | E0B07DDF1C3E52DA001CCAB7 /* WebViewContentPositioner.framework in Frameworks */, 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | E0B07DB61C3E5230001CCAB7 = { 79 | isa = PBXGroup; 80 | children = ( 81 | E0B07DD61C3E52D0001CCAB7 /* WebViewContentPositioner.xcodeproj */, 82 | E0B07DC11C3E5230001CCAB7 /* Demo */, 83 | E0B07DC01C3E5230001CCAB7 /* Products */, 84 | ); 85 | sourceTree = ""; 86 | }; 87 | E0B07DC01C3E5230001CCAB7 /* Products */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | E0B07DBF1C3E5230001CCAB7 /* Demo.app */, 91 | ); 92 | name = Products; 93 | sourceTree = ""; 94 | }; 95 | E0B07DC11C3E5230001CCAB7 /* Demo */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | E0B07DC21C3E5230001CCAB7 /* AppDelegate.swift */, 99 | E0B07DC41C3E5230001CCAB7 /* ViewController.swift */, 100 | E0B07DC91C3E5230001CCAB7 /* Assets.xcassets */, 101 | E0B07DCB1C3E5230001CCAB7 /* LaunchScreen.storyboard */, 102 | E0B07DCE1C3E5230001CCAB7 /* Info.plist */, 103 | ); 104 | path = Demo; 105 | sourceTree = ""; 106 | }; 107 | E0B07DD71C3E52D0001CCAB7 /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | E0B07DDC1C3E52D0001CCAB7 /* WebViewContentPositioner.framework */, 111 | E0B07DDE1C3E52D0001CCAB7 /* WebViewContentPositionerTests.xctest */, 112 | ); 113 | name = Products; 114 | sourceTree = ""; 115 | }; 116 | /* End PBXGroup section */ 117 | 118 | /* Begin PBXNativeTarget section */ 119 | E0B07DBE1C3E5230001CCAB7 /* Demo */ = { 120 | isa = PBXNativeTarget; 121 | buildConfigurationList = E0B07DD11C3E5230001CCAB7 /* Build configuration list for PBXNativeTarget "Demo" */; 122 | buildPhases = ( 123 | E0B07DBB1C3E5230001CCAB7 /* Sources */, 124 | E0B07DBC1C3E5230001CCAB7 /* Frameworks */, 125 | E0B07DBD1C3E5230001CCAB7 /* Resources */, 126 | E0B07DE31C3E52DA001CCAB7 /* Embed Frameworks */, 127 | ); 128 | buildRules = ( 129 | ); 130 | dependencies = ( 131 | E0B07DE21C3E52DA001CCAB7 /* PBXTargetDependency */, 132 | ); 133 | name = Demo; 134 | productName = Demo; 135 | productReference = E0B07DBF1C3E5230001CCAB7 /* Demo.app */; 136 | productType = "com.apple.product-type.application"; 137 | }; 138 | /* End PBXNativeTarget section */ 139 | 140 | /* Begin PBXProject section */ 141 | E0B07DB71C3E5230001CCAB7 /* Project object */ = { 142 | isa = PBXProject; 143 | attributes = { 144 | LastSwiftUpdateCheck = 0720; 145 | LastUpgradeCheck = 0800; 146 | ORGANIZATIONNAME = lazyapps; 147 | TargetAttributes = { 148 | E0B07DBE1C3E5230001CCAB7 = { 149 | CreatedOnToolsVersion = 7.2; 150 | LastSwiftMigration = 0800; 151 | }; 152 | }; 153 | }; 154 | buildConfigurationList = E0B07DBA1C3E5230001CCAB7 /* Build configuration list for PBXProject "Demo" */; 155 | compatibilityVersion = "Xcode 3.2"; 156 | developmentRegion = English; 157 | hasScannedForEncodings = 0; 158 | knownRegions = ( 159 | en, 160 | Base, 161 | ); 162 | mainGroup = E0B07DB61C3E5230001CCAB7; 163 | productRefGroup = E0B07DC01C3E5230001CCAB7 /* Products */; 164 | projectDirPath = ""; 165 | projectReferences = ( 166 | { 167 | ProductGroup = E0B07DD71C3E52D0001CCAB7 /* Products */; 168 | ProjectRef = E0B07DD61C3E52D0001CCAB7 /* WebViewContentPositioner.xcodeproj */; 169 | }, 170 | ); 171 | projectRoot = ""; 172 | targets = ( 173 | E0B07DBE1C3E5230001CCAB7 /* Demo */, 174 | ); 175 | }; 176 | /* End PBXProject section */ 177 | 178 | /* Begin PBXReferenceProxy section */ 179 | E0B07DDC1C3E52D0001CCAB7 /* WebViewContentPositioner.framework */ = { 180 | isa = PBXReferenceProxy; 181 | fileType = wrapper.framework; 182 | path = WebViewContentPositioner.framework; 183 | remoteRef = E0B07DDB1C3E52D0001CCAB7 /* PBXContainerItemProxy */; 184 | sourceTree = BUILT_PRODUCTS_DIR; 185 | }; 186 | E0B07DDE1C3E52D0001CCAB7 /* WebViewContentPositionerTests.xctest */ = { 187 | isa = PBXReferenceProxy; 188 | fileType = wrapper.cfbundle; 189 | path = WebViewContentPositionerTests.xctest; 190 | remoteRef = E0B07DDD1C3E52D0001CCAB7 /* PBXContainerItemProxy */; 191 | sourceTree = BUILT_PRODUCTS_DIR; 192 | }; 193 | /* End PBXReferenceProxy section */ 194 | 195 | /* Begin PBXResourcesBuildPhase section */ 196 | E0B07DBD1C3E5230001CCAB7 /* Resources */ = { 197 | isa = PBXResourcesBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | E0B07DCD1C3E5230001CCAB7 /* LaunchScreen.storyboard in Resources */, 201 | E0B07DCA1C3E5230001CCAB7 /* Assets.xcassets in Resources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXResourcesBuildPhase section */ 206 | 207 | /* Begin PBXSourcesBuildPhase section */ 208 | E0B07DBB1C3E5230001CCAB7 /* Sources */ = { 209 | isa = PBXSourcesBuildPhase; 210 | buildActionMask = 2147483647; 211 | files = ( 212 | E0B07DC51C3E5230001CCAB7 /* ViewController.swift in Sources */, 213 | E0B07DC31C3E5230001CCAB7 /* AppDelegate.swift in Sources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXSourcesBuildPhase section */ 218 | 219 | /* Begin PBXTargetDependency section */ 220 | E0B07DE21C3E52DA001CCAB7 /* PBXTargetDependency */ = { 221 | isa = PBXTargetDependency; 222 | name = WebViewContentPositioner; 223 | targetProxy = E0B07DE11C3E52DA001CCAB7 /* PBXContainerItemProxy */; 224 | }; 225 | /* End PBXTargetDependency section */ 226 | 227 | /* Begin PBXVariantGroup section */ 228 | E0B07DCB1C3E5230001CCAB7 /* LaunchScreen.storyboard */ = { 229 | isa = PBXVariantGroup; 230 | children = ( 231 | E0B07DCC1C3E5230001CCAB7 /* Base */, 232 | ); 233 | name = LaunchScreen.storyboard; 234 | sourceTree = ""; 235 | }; 236 | /* End PBXVariantGroup section */ 237 | 238 | /* Begin XCBuildConfiguration section */ 239 | E0B07DCF1C3E5230001CCAB7 /* Debug */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | ALWAYS_SEARCH_USER_PATHS = NO; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_WARN_BOOL_CONVERSION = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 250 | CLANG_WARN_EMPTY_BODY = YES; 251 | CLANG_WARN_ENUM_CONVERSION = YES; 252 | CLANG_WARN_INFINITE_RECURSION = YES; 253 | CLANG_WARN_INT_CONVERSION = YES; 254 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 255 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 256 | CLANG_WARN_UNREACHABLE_CODE = YES; 257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 258 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 259 | COPY_PHASE_STRIP = NO; 260 | DEBUG_INFORMATION_FORMAT = dwarf; 261 | ENABLE_STRICT_OBJC_MSGSEND = YES; 262 | ENABLE_TESTABILITY = YES; 263 | GCC_C_LANGUAGE_STANDARD = gnu99; 264 | GCC_DYNAMIC_NO_PIC = NO; 265 | GCC_NO_COMMON_BLOCKS = YES; 266 | GCC_OPTIMIZATION_LEVEL = 0; 267 | GCC_PREPROCESSOR_DEFINITIONS = ( 268 | "DEBUG=1", 269 | "$(inherited)", 270 | ); 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 278 | MTL_ENABLE_DEBUG_INFO = YES; 279 | ONLY_ACTIVE_ARCH = YES; 280 | SDKROOT = iphoneos; 281 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 282 | TARGETED_DEVICE_FAMILY = "1,2"; 283 | }; 284 | name = Debug; 285 | }; 286 | E0B07DD01C3E5230001CCAB7 /* Release */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ALWAYS_SEARCH_USER_PATHS = NO; 290 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 291 | CLANG_CXX_LIBRARY = "libc++"; 292 | CLANG_ENABLE_MODULES = YES; 293 | CLANG_ENABLE_OBJC_ARC = YES; 294 | CLANG_WARN_BOOL_CONVERSION = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 297 | CLANG_WARN_EMPTY_BODY = YES; 298 | CLANG_WARN_ENUM_CONVERSION = YES; 299 | CLANG_WARN_INFINITE_RECURSION = YES; 300 | CLANG_WARN_INT_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 303 | CLANG_WARN_UNREACHABLE_CODE = YES; 304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 305 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 306 | COPY_PHASE_STRIP = NO; 307 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 308 | ENABLE_NS_ASSERTIONS = NO; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | GCC_C_LANGUAGE_STANDARD = gnu99; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 319 | MTL_ENABLE_DEBUG_INFO = NO; 320 | SDKROOT = iphoneos; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | VALIDATE_PRODUCT = YES; 324 | }; 325 | name = Release; 326 | }; 327 | E0B07DD21C3E5230001CCAB7 /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 332 | INFOPLIST_FILE = Demo/Info.plist; 333 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 334 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 335 | PRODUCT_BUNDLE_IDENTIFIER = com.lazyapps.WebViewContentPositionerDemo; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | SWIFT_VERSION = 2.3; 338 | }; 339 | name = Debug; 340 | }; 341 | E0B07DD31C3E5230001CCAB7 /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 346 | INFOPLIST_FILE = Demo/Info.plist; 347 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 348 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 349 | PRODUCT_BUNDLE_IDENTIFIER = com.lazyapps.WebViewContentPositionerDemo; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | SWIFT_VERSION = 2.3; 352 | }; 353 | name = Release; 354 | }; 355 | /* End XCBuildConfiguration section */ 356 | 357 | /* Begin XCConfigurationList section */ 358 | E0B07DBA1C3E5230001CCAB7 /* Build configuration list for PBXProject "Demo" */ = { 359 | isa = XCConfigurationList; 360 | buildConfigurations = ( 361 | E0B07DCF1C3E5230001CCAB7 /* Debug */, 362 | E0B07DD01C3E5230001CCAB7 /* Release */, 363 | ); 364 | defaultConfigurationIsVisible = 0; 365 | defaultConfigurationName = Release; 366 | }; 367 | E0B07DD11C3E5230001CCAB7 /* Build configuration list for PBXNativeTarget "Demo" */ = { 368 | isa = XCConfigurationList; 369 | buildConfigurations = ( 370 | E0B07DD21C3E5230001CCAB7 /* Debug */, 371 | E0B07DD31C3E5230001CCAB7 /* Release */, 372 | ); 373 | defaultConfigurationIsVisible = 0; 374 | defaultConfigurationName = Release; 375 | }; 376 | /* End XCConfigurationList section */ 377 | }; 378 | rootObject = E0B07DB71C3E5230001CCAB7 /* Project object */; 379 | } 380 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by CHEN Xian’an on 1/7/16. 6 | // Copyright © 2016 lazyapps. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.mainScreen().bounds) 18 | let vc = ViewController() 19 | vc.title = "WebViewContentPositioner Demo" 20 | let nvc = UINavigationController(rootViewController: vc) 21 | nvc.toolbarHidden = false 22 | window?.rootViewController = nvc 23 | window?.makeKeyAndVisible() 24 | return true 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Demo/Demo/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 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Demo/Demo/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 | -------------------------------------------------------------------------------- /Demo/Demo/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Demo/Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo 4 | // 5 | // Created by CHEN Xian’an on 1/7/16. 6 | // Copyright © 2016 lazyapps. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebViewContentPositioner 11 | 12 | private let userDefaultsPositionKey = "userDefaultsPositionKey" 13 | 14 | class ViewController: UIViewController { 15 | 16 | private lazy var webView: UIWebView = { 17 | let w = UIWebView(frame: CGRectZero) 18 | w.translatesAutoresizingMaskIntoConstraints = false 19 | w.delegate = self 20 | return w 21 | }() 22 | 23 | override func loadView() { 24 | super.loadView() 25 | view.addSubview(webView) 26 | _setupConstraintLayouts() 27 | _setupToolbarItems() 28 | guard let url = NSURL(string: "https://en.m.wikipedia.org/wiki/Native_Americans_in_the_United_States") else { fatalError("Fail to construct URL") } 29 | webView.loadRequest(NSURLRequest(URL: url)) 30 | } 31 | 32 | } 33 | 34 | extension ViewController: UIWebViewDelegate { 35 | 36 | func webViewDidFinishLoad(webView: UIWebView) { 37 | toolbarItems?.forEach { $0.enabled = true } 38 | } 39 | 40 | } 41 | 42 | private extension ViewController { 43 | 44 | func _setupConstraintLayouts() { 45 | NSLayoutConstraint.activateConstraints([ 46 | webView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor), 47 | webView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor), 48 | webView.topAnchor.constraintEqualToAnchor(view.topAnchor), 49 | webView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor) 50 | ]) 51 | } 52 | 53 | func _setupToolbarItems() { 54 | let savePositionItem = UIBarButtonItem(title: "Save Position", style: .Plain, target: self, action: #selector(ViewController.saveCurrentPosition(_:))) 55 | let restorePositionItem = UIBarButtonItem(title: "Restore Position", style: .Plain, target: self, action: #selector(ViewController.restorePosition(_:))) 56 | let flexItem = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target: nil, action: nil) 57 | toolbarItems = [savePositionItem, flexItem, restorePositionItem] 58 | toolbarItems?.forEach { $0.enabled = false } 59 | } 60 | 61 | func _showAlertWithTitle(title: String?, message: String?) { 62 | let ac = UIAlertController(title: title, message: message, preferredStyle: .Alert) 63 | ac.addAction(UIAlertAction(title: "Dismiss", style: .Cancel, handler: { _ in })) 64 | presentViewController(ac, animated: true, completion: nil) 65 | } 66 | 67 | @objc func saveCurrentPosition(sender: AnyObject?) { 68 | guard let jsonStr = webView.currentPosition() else { return } 69 | print(jsonStr) 70 | let ud = NSUserDefaults.standardUserDefaults() 71 | ud.setObject(jsonStr, forKey: userDefaultsPositionKey) 72 | ud.synchronize() 73 | _showAlertWithTitle("Position JSON String Saved to NSUserDefaults", message: "Relaunch app and tap “Restore Position” button to restore saved position.") 74 | } 75 | 76 | @objc func restorePosition(sender: AnyObject?) { 77 | guard let jsonStr = NSUserDefaults.standardUserDefaults().objectForKey(userDefaultsPositionKey) as? JSONString else { 78 | _showAlertWithTitle("No Saved Position", message: nil) 79 | return 80 | } 81 | 82 | webView.restorePosition(jsonStr) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 — Present CHEN Xian-an 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 furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | 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 | 21 | ===================================================================== 22 | 23 | Software License Agreement (BSD License) 24 | 25 | Copyright (c) 2009, Mozilla Foundation 26 | All rights reserved. 27 | 28 | Redistribution and use of this software in source and binary forms, with or without modification, 29 | are permitted provided that the following conditions are met: 30 | 31 | * Redistributions of source code must retain the above 32 | copyright notice, this list of conditions and the 33 | following disclaimer. 34 | 35 | * Redistributions in binary form must reproduce the above 36 | copyright notice, this list of conditions and the 37 | following disclaimer in the documentation and/or other 38 | materials provided with the distribution. 39 | 40 | * Neither the name of Mozilla Foundation nor the names of its 41 | contributors may be used to endorse or promote products 42 | derived from this software without specific prior 43 | written permission of Mozilla Foundation. 44 | 45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 46 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 47 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 48 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 49 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 50 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 51 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 52 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebViewContentPositioner 2 | 3 | `WebViewContentPositioner` is a scroll position maintainer for `UIWebView`. Never losing scroll position when view resizing on device rotation, split window changing, etc., even if view restoration. 4 | 5 | ## How to Use 6 | 7 | 1. Add the `WebViewContentPositioner` repository as a submodule of your application’s repository. 8 | 2. Drag and drop `WebViewContentPositioner.xcodeproj` into your application’s Xcode project or workspace. 9 | 3. On the “General” tab of your application target’s settings, add `WebViewContentPositioner.framework` to the “Embedded Binaries” section. 10 | 11 | Or, If you would prefer to use Carthage or CocoaPods, please pull request. 12 | 13 | `WebViewContentPositioner` tracks scroll position automatically for device rotation and split window size changing (by swizzling `- traitCollectionDidChange:`). Plus, you can save current position as a JSON String by `[UIWebView -currentPosition]` and restore by `[UIWebView -restorePosition:]` if needed. 14 | 15 | See a real usage inside `Demo` folder. 16 | 17 | 18 | ## Gotcha 19 | 20 | `WebViewContentPositioner` may fail if web page contains position-fixed elements on top. It's not intent to be used in a browser but EPUB reader like project. 21 | 22 | ## About Me 23 | 24 | * Twitter: [@_cxa](https://twitter.com/_cxa) 25 | * Apps available in App Store: 26 | * PayPal: xianan.chen+paypal 📧 gmail.com, buy me a cup of coffee if you find it's useful for you. 27 | 28 | ## License 29 | 30 | `DOMContentLoadedDelegate` is released under the MIT license. In short, it's royalty-free but you must keep the copyright notice in your code or software distribution. 31 | 32 | Some functions in JS are derived from [Firebug](https://github.com/firebug/firebug), it is free and open source software distributed under the BSD License. 33 | -------------------------------------------------------------------------------- /WebViewContentPositioner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E0B07D6C1C3D40C1001CCAB7 /* WebViewContentPositioner.h in Headers */ = {isa = PBXBuildFile; fileRef = E0B07D6B1C3D40C1001CCAB7 /* WebViewContentPositioner.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | E0B07D731C3D40C1001CCAB7 /* WebViewContentPositioner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0B07D681C3D40C1001CCAB7 /* WebViewContentPositioner.framework */; }; 12 | E0B07D781C3D40C1001CCAB7 /* WebViewContentPositionerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B07D771C3D40C1001CCAB7 /* WebViewContentPositionerTests.swift */; }; 13 | E0B07D851C3D424F001CCAB7 /* UIWebView+WebViewContentPositioner.m in Sources */ = {isa = PBXBuildFile; fileRef = E0B07D831C3D424F001CCAB7 /* UIWebView+WebViewContentPositioner.m */; }; 14 | E0B07D871C3D42DF001CCAB7 /* UIWebView+WebViewContentPositioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B07D861C3D42DF001CCAB7 /* UIWebView+WebViewContentPositioner.swift */; }; 15 | E0B07D891C3D456E001CCAB7 /* positioner.js in Resources */ = {isa = PBXBuildFile; fileRef = E0B07D881C3D456E001CCAB7 /* positioner.js */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXContainerItemProxy section */ 19 | E0B07D741C3D40C1001CCAB7 /* PBXContainerItemProxy */ = { 20 | isa = PBXContainerItemProxy; 21 | containerPortal = E0B07D5F1C3D40C1001CCAB7 /* Project object */; 22 | proxyType = 1; 23 | remoteGlobalIDString = E0B07D671C3D40C1001CCAB7; 24 | remoteInfo = WebViewContentPositioner; 25 | }; 26 | /* End PBXContainerItemProxy section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | E0B07D681C3D40C1001CCAB7 /* WebViewContentPositioner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WebViewContentPositioner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | E0B07D6B1C3D40C1001CCAB7 /* WebViewContentPositioner.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WebViewContentPositioner.h; sourceTree = ""; }; 31 | E0B07D6D1C3D40C1001CCAB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | E0B07D721C3D40C1001CCAB7 /* WebViewContentPositionerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WebViewContentPositionerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | E0B07D771C3D40C1001CCAB7 /* WebViewContentPositionerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewContentPositionerTests.swift; sourceTree = ""; }; 34 | E0B07D791C3D40C1001CCAB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | E0B07D831C3D424F001CCAB7 /* UIWebView+WebViewContentPositioner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWebView+WebViewContentPositioner.m"; sourceTree = ""; }; 36 | E0B07D861C3D42DF001CCAB7 /* UIWebView+WebViewContentPositioner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIWebView+WebViewContentPositioner.swift"; sourceTree = ""; }; 37 | E0B07D881C3D456E001CCAB7 /* positioner.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = positioner.js; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | E0B07D641C3D40C1001CCAB7 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | E0B07D6F1C3D40C1001CCAB7 /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | E0B07D731C3D40C1001CCAB7 /* WebViewContentPositioner.framework in Frameworks */, 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXFrameworksBuildPhase section */ 57 | 58 | /* Begin PBXGroup section */ 59 | E0B07D5E1C3D40C1001CCAB7 = { 60 | isa = PBXGroup; 61 | children = ( 62 | E0B07D6A1C3D40C1001CCAB7 /* WebViewContentPositioner */, 63 | E0B07D761C3D40C1001CCAB7 /* WebViewContentPositionerTests */, 64 | E0B07D691C3D40C1001CCAB7 /* Products */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | E0B07D691C3D40C1001CCAB7 /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | E0B07D681C3D40C1001CCAB7 /* WebViewContentPositioner.framework */, 72 | E0B07D721C3D40C1001CCAB7 /* WebViewContentPositionerTests.xctest */, 73 | ); 74 | name = Products; 75 | sourceTree = ""; 76 | }; 77 | E0B07D6A1C3D40C1001CCAB7 /* WebViewContentPositioner */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | E0B07D6D1C3D40C1001CCAB7 /* Info.plist */, 81 | E0B07D6B1C3D40C1001CCAB7 /* WebViewContentPositioner.h */, 82 | E0B07D831C3D424F001CCAB7 /* UIWebView+WebViewContentPositioner.m */, 83 | E0B07D861C3D42DF001CCAB7 /* UIWebView+WebViewContentPositioner.swift */, 84 | E0B07D881C3D456E001CCAB7 /* positioner.js */, 85 | ); 86 | path = WebViewContentPositioner; 87 | sourceTree = ""; 88 | }; 89 | E0B07D761C3D40C1001CCAB7 /* WebViewContentPositionerTests */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | E0B07D771C3D40C1001CCAB7 /* WebViewContentPositionerTests.swift */, 93 | E0B07D791C3D40C1001CCAB7 /* Info.plist */, 94 | ); 95 | path = WebViewContentPositionerTests; 96 | sourceTree = ""; 97 | }; 98 | /* End PBXGroup section */ 99 | 100 | /* Begin PBXHeadersBuildPhase section */ 101 | E0B07D651C3D40C1001CCAB7 /* Headers */ = { 102 | isa = PBXHeadersBuildPhase; 103 | buildActionMask = 2147483647; 104 | files = ( 105 | E0B07D6C1C3D40C1001CCAB7 /* WebViewContentPositioner.h in Headers */, 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | /* End PBXHeadersBuildPhase section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | E0B07D671C3D40C1001CCAB7 /* WebViewContentPositioner */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = E0B07D7C1C3D40C1001CCAB7 /* Build configuration list for PBXNativeTarget "WebViewContentPositioner" */; 115 | buildPhases = ( 116 | E0B07D631C3D40C1001CCAB7 /* Sources */, 117 | E0B07D641C3D40C1001CCAB7 /* Frameworks */, 118 | E0B07D651C3D40C1001CCAB7 /* Headers */, 119 | E0B07D661C3D40C1001CCAB7 /* Resources */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = WebViewContentPositioner; 126 | productName = WebViewContentPositioner; 127 | productReference = E0B07D681C3D40C1001CCAB7 /* WebViewContentPositioner.framework */; 128 | productType = "com.apple.product-type.framework"; 129 | }; 130 | E0B07D711C3D40C1001CCAB7 /* WebViewContentPositionerTests */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = E0B07D7F1C3D40C1001CCAB7 /* Build configuration list for PBXNativeTarget "WebViewContentPositionerTests" */; 133 | buildPhases = ( 134 | E0B07D6E1C3D40C1001CCAB7 /* Sources */, 135 | E0B07D6F1C3D40C1001CCAB7 /* Frameworks */, 136 | E0B07D701C3D40C1001CCAB7 /* Resources */, 137 | ); 138 | buildRules = ( 139 | ); 140 | dependencies = ( 141 | E0B07D751C3D40C1001CCAB7 /* PBXTargetDependency */, 142 | ); 143 | name = WebViewContentPositionerTests; 144 | productName = WebViewContentPositionerTests; 145 | productReference = E0B07D721C3D40C1001CCAB7 /* WebViewContentPositionerTests.xctest */; 146 | productType = "com.apple.product-type.bundle.unit-test"; 147 | }; 148 | /* End PBXNativeTarget section */ 149 | 150 | /* Begin PBXProject section */ 151 | E0B07D5F1C3D40C1001CCAB7 /* Project object */ = { 152 | isa = PBXProject; 153 | attributes = { 154 | LastSwiftMigration = 0730; 155 | LastSwiftUpdateCheck = 0720; 156 | LastUpgradeCheck = 0800; 157 | ORGANIZATIONNAME = lazyapps; 158 | TargetAttributes = { 159 | E0B07D671C3D40C1001CCAB7 = { 160 | CreatedOnToolsVersion = 7.2; 161 | LastSwiftMigration = 0800; 162 | }; 163 | E0B07D711C3D40C1001CCAB7 = { 164 | CreatedOnToolsVersion = 7.2; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = E0B07D621C3D40C1001CCAB7 /* Build configuration list for PBXProject "WebViewContentPositioner" */; 169 | compatibilityVersion = "Xcode 3.2"; 170 | developmentRegion = English; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | ); 175 | mainGroup = E0B07D5E1C3D40C1001CCAB7; 176 | productRefGroup = E0B07D691C3D40C1001CCAB7 /* Products */; 177 | projectDirPath = ""; 178 | projectRoot = ""; 179 | targets = ( 180 | E0B07D671C3D40C1001CCAB7 /* WebViewContentPositioner */, 181 | E0B07D711C3D40C1001CCAB7 /* WebViewContentPositionerTests */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | E0B07D661C3D40C1001CCAB7 /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | E0B07D891C3D456E001CCAB7 /* positioner.js in Resources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | E0B07D701C3D40C1001CCAB7 /* Resources */ = { 196 | isa = PBXResourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | }; 202 | /* End PBXResourcesBuildPhase section */ 203 | 204 | /* Begin PBXSourcesBuildPhase section */ 205 | E0B07D631C3D40C1001CCAB7 /* Sources */ = { 206 | isa = PBXSourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | E0B07D851C3D424F001CCAB7 /* UIWebView+WebViewContentPositioner.m in Sources */, 210 | E0B07D871C3D42DF001CCAB7 /* UIWebView+WebViewContentPositioner.swift in Sources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | E0B07D6E1C3D40C1001CCAB7 /* Sources */ = { 215 | isa = PBXSourcesBuildPhase; 216 | buildActionMask = 2147483647; 217 | files = ( 218 | E0B07D781C3D40C1001CCAB7 /* WebViewContentPositionerTests.swift in Sources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | /* End PBXSourcesBuildPhase section */ 223 | 224 | /* Begin PBXTargetDependency section */ 225 | E0B07D751C3D40C1001CCAB7 /* PBXTargetDependency */ = { 226 | isa = PBXTargetDependency; 227 | target = E0B07D671C3D40C1001CCAB7 /* WebViewContentPositioner */; 228 | targetProxy = E0B07D741C3D40C1001CCAB7 /* PBXContainerItemProxy */; 229 | }; 230 | /* End PBXTargetDependency section */ 231 | 232 | /* Begin XCBuildConfiguration section */ 233 | E0B07D7A1C3D40C1001CCAB7 /* Debug */ = { 234 | isa = XCBuildConfiguration; 235 | buildSettings = { 236 | ALWAYS_SEARCH_USER_PATHS = NO; 237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 238 | CLANG_CXX_LIBRARY = "libc++"; 239 | CLANG_ENABLE_MODULES = YES; 240 | CLANG_ENABLE_OBJC_ARC = YES; 241 | CLANG_WARN_BOOL_CONVERSION = YES; 242 | CLANG_WARN_CONSTANT_CONVERSION = YES; 243 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 244 | CLANG_WARN_EMPTY_BODY = YES; 245 | CLANG_WARN_ENUM_CONVERSION = YES; 246 | CLANG_WARN_INFINITE_RECURSION = YES; 247 | CLANG_WARN_INT_CONVERSION = YES; 248 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 249 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 250 | CLANG_WARN_UNREACHABLE_CODE = YES; 251 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 252 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 253 | COPY_PHASE_STRIP = NO; 254 | CURRENT_PROJECT_VERSION = 1; 255 | DEBUG_INFORMATION_FORMAT = dwarf; 256 | ENABLE_STRICT_OBJC_MSGSEND = YES; 257 | ENABLE_TESTABILITY = YES; 258 | GCC_C_LANGUAGE_STANDARD = gnu99; 259 | GCC_DYNAMIC_NO_PIC = NO; 260 | GCC_NO_COMMON_BLOCKS = YES; 261 | GCC_OPTIMIZATION_LEVEL = 0; 262 | GCC_PREPROCESSOR_DEFINITIONS = ( 263 | "DEBUG=1", 264 | "$(inherited)", 265 | ); 266 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 267 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 268 | GCC_WARN_UNDECLARED_SELECTOR = YES; 269 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 270 | GCC_WARN_UNUSED_FUNCTION = YES; 271 | GCC_WARN_UNUSED_VARIABLE = YES; 272 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 273 | MTL_ENABLE_DEBUG_INFO = YES; 274 | ONLY_ACTIVE_ARCH = YES; 275 | SDKROOT = iphoneos; 276 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 277 | TARGETED_DEVICE_FAMILY = "1,2"; 278 | VERSIONING_SYSTEM = "apple-generic"; 279 | VERSION_INFO_PREFIX = ""; 280 | }; 281 | name = Debug; 282 | }; 283 | E0B07D7B1C3D40C1001CCAB7 /* Release */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | ALWAYS_SEARCH_USER_PATHS = NO; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_WARN_BOOL_CONVERSION = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 299 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 300 | CLANG_WARN_UNREACHABLE_CODE = YES; 301 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 302 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 303 | COPY_PHASE_STRIP = NO; 304 | CURRENT_PROJECT_VERSION = 1; 305 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 306 | ENABLE_NS_ASSERTIONS = NO; 307 | ENABLE_STRICT_OBJC_MSGSEND = YES; 308 | GCC_C_LANGUAGE_STANDARD = gnu99; 309 | GCC_NO_COMMON_BLOCKS = YES; 310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 312 | GCC_WARN_UNDECLARED_SELECTOR = YES; 313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 314 | GCC_WARN_UNUSED_FUNCTION = YES; 315 | GCC_WARN_UNUSED_VARIABLE = YES; 316 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 317 | MTL_ENABLE_DEBUG_INFO = NO; 318 | SDKROOT = iphoneos; 319 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 320 | TARGETED_DEVICE_FAMILY = "1,2"; 321 | VALIDATE_PRODUCT = YES; 322 | VERSIONING_SYSTEM = "apple-generic"; 323 | VERSION_INFO_PREFIX = ""; 324 | }; 325 | name = Release; 326 | }; 327 | E0B07D7D1C3D40C1001CCAB7 /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | CLANG_ENABLE_MODULES = YES; 331 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 332 | DEFINES_MODULE = YES; 333 | DYLIB_COMPATIBILITY_VERSION = 1; 334 | DYLIB_CURRENT_VERSION = 1; 335 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 336 | INFOPLIST_FILE = WebViewContentPositioner/Info.plist; 337 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 338 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 339 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 340 | PRODUCT_BUNDLE_IDENTIFIER = com.lazyapps.WebViewContentPositioner; 341 | PRODUCT_NAME = "$(TARGET_NAME)"; 342 | SKIP_INSTALL = YES; 343 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 344 | SWIFT_VERSION = 2.3; 345 | }; 346 | name = Debug; 347 | }; 348 | E0B07D7E1C3D40C1001CCAB7 /* Release */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | CLANG_ENABLE_MODULES = YES; 352 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 353 | DEFINES_MODULE = YES; 354 | DYLIB_COMPATIBILITY_VERSION = 1; 355 | DYLIB_CURRENT_VERSION = 1; 356 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 357 | INFOPLIST_FILE = WebViewContentPositioner/Info.plist; 358 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 359 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 360 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 361 | PRODUCT_BUNDLE_IDENTIFIER = com.lazyapps.WebViewContentPositioner; 362 | PRODUCT_NAME = "$(TARGET_NAME)"; 363 | SKIP_INSTALL = YES; 364 | SWIFT_VERSION = 2.3; 365 | }; 366 | name = Release; 367 | }; 368 | E0B07D801C3D40C1001CCAB7 /* Debug */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | INFOPLIST_FILE = WebViewContentPositionerTests/Info.plist; 372 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 373 | PRODUCT_BUNDLE_IDENTIFIER = com.lazyapps.WebViewContentPositionerTests; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | }; 376 | name = Debug; 377 | }; 378 | E0B07D811C3D40C1001CCAB7 /* Release */ = { 379 | isa = XCBuildConfiguration; 380 | buildSettings = { 381 | INFOPLIST_FILE = WebViewContentPositionerTests/Info.plist; 382 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 383 | PRODUCT_BUNDLE_IDENTIFIER = com.lazyapps.WebViewContentPositionerTests; 384 | PRODUCT_NAME = "$(TARGET_NAME)"; 385 | }; 386 | name = Release; 387 | }; 388 | /* End XCBuildConfiguration section */ 389 | 390 | /* Begin XCConfigurationList section */ 391 | E0B07D621C3D40C1001CCAB7 /* Build configuration list for PBXProject "WebViewContentPositioner" */ = { 392 | isa = XCConfigurationList; 393 | buildConfigurations = ( 394 | E0B07D7A1C3D40C1001CCAB7 /* Debug */, 395 | E0B07D7B1C3D40C1001CCAB7 /* Release */, 396 | ); 397 | defaultConfigurationIsVisible = 0; 398 | defaultConfigurationName = Release; 399 | }; 400 | E0B07D7C1C3D40C1001CCAB7 /* Build configuration list for PBXNativeTarget "WebViewContentPositioner" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | E0B07D7D1C3D40C1001CCAB7 /* Debug */, 404 | E0B07D7E1C3D40C1001CCAB7 /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | E0B07D7F1C3D40C1001CCAB7 /* Build configuration list for PBXNativeTarget "WebViewContentPositionerTests" */ = { 410 | isa = XCConfigurationList; 411 | buildConfigurations = ( 412 | E0B07D801C3D40C1001CCAB7 /* Debug */, 413 | E0B07D811C3D40C1001CCAB7 /* Release */, 414 | ); 415 | defaultConfigurationIsVisible = 0; 416 | defaultConfigurationName = Release; 417 | }; 418 | /* End XCConfigurationList section */ 419 | }; 420 | rootObject = E0B07D5F1C3D40C1001CCAB7 /* Project object */; 421 | } 422 | -------------------------------------------------------------------------------- /WebViewContentPositioner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WebViewContentPositioner/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 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /WebViewContentPositioner/UIWebView+WebViewContentPositioner.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIWebView+WebViewContentPositioner.m 3 | // WebViewContentPositioner 4 | // 5 | // Created by CHEN Xian’an on 1/6/16. 6 | // Copyright © 2016 lazyapps. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | @import ObjectiveC.runtime; 11 | 12 | static void (*origTraitCollectionDidChange)(id, SEL, UITraitCollection *); 13 | static void newTraitCollectionDidChange(id, SEL, UITraitCollection *); 14 | 15 | @implementation UIWebView (WebViewContentPositioner) 16 | 17 | + (void)load 18 | { 19 | static dispatch_once_t onceToken; 20 | dispatch_once(&onceToken, ^{ 21 | Method origMethod = class_getInstanceMethod(self, @selector(traitCollectionDidChange:)); 22 | origTraitCollectionDidChange = (void *)method_getImplementation(origMethod); 23 | if (!class_addMethod(self, @selector(traitCollectionDidChange:), (IMP)newTraitCollectionDidChange, method_getTypeEncoding(origMethod))) 24 | method_setImplementation(origMethod, (IMP)newTraitCollectionDidChange); 25 | }); 26 | } 27 | 28 | @end 29 | 30 | void newTraitCollectionDidChange(id self, SEL cmd, UITraitCollection *tc) { 31 | origTraitCollectionDidChange(self, cmd, tc); 32 | #pragma clang diagnostic push 33 | #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 34 | [self performSelector:NSSelectorFromString(@"WebViewContentPositioner_traitCollectionDidChange:") withObject:tc]; 35 | #pragma clang diagnostic pop 36 | } -------------------------------------------------------------------------------- /WebViewContentPositioner/UIWebView+WebViewContentPositioner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWebView+WebViewContentPositioner.swift 3 | // WebViewContentPositioner 4 | // 5 | // Created by CHEN Xian’an on 1/6/16. 6 | // Copyright © 2016 lazyapps. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import JavaScriptCore 11 | 12 | public typealias JSONString = String 13 | 14 | public extension UIWebView { 15 | 16 | func currentPosition() -> JSONString? { 17 | return stringByEvaluatingJavaScriptFromString("JSON.stringify(WebViewContentPositioner.currentPosition())") 18 | } 19 | 20 | func restorePosition(position: JSONString) { 21 | stringByEvaluatingJavaScriptFromString("WebViewContentPositioner.restorePosition(\(position))") 22 | } 23 | 24 | } 25 | 26 | private extension UIWebView { 27 | 28 | var context: JSContext? { 29 | return valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext 30 | } 31 | 32 | func evalJSIfNeeded() { 33 | guard 34 | context?.objectForKeyedSubscript("WebViewContentPositioner").isUndefined != false, 35 | let bundle = NSBundle(identifier: "com.lazyapps.WebViewContentPositioner"), 36 | let jsFileURL = bundle.URLForResource("positioner", withExtension: "js"), 37 | let script = try? NSString(contentsOfURL: jsFileURL, encoding: NSUTF8StringEncoding) 38 | else { return } 39 | 40 | context?.evaluateScript(script as String) 41 | } 42 | 43 | @objc func WebViewContentPositioner_traitCollectionDidChange(previousTraitCollection: UITraitCollection?) { 44 | guard traitCollection != previousTraitCollection else { return } 45 | evalJSIfNeeded() 46 | // This method involve DOM modification, it's safe to call with `stringByEvaluatingJavaScriptFromString` but not `evaluateScript` of `JSContext` 47 | stringByEvaluatingJavaScriptFromString("WebViewContentPositioner.restorePosition()") 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /WebViewContentPositioner/WebViewContentPositioner.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewContentPositioner.h 3 | // WebViewContentPositioner 4 | // 5 | // Created by CHEN Xian’an on 1/6/16. 6 | // Copyright © 2016 lazyapps. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for WebViewContentPositioner. 12 | FOUNDATION_EXPORT double WebViewContentPositionerVersionNumber; 13 | 14 | //! Project version string for WebViewContentPositioner. 15 | FOUNDATION_EXPORT const unsigned char WebViewContentPositionerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /WebViewContentPositioner/positioner.js: -------------------------------------------------------------------------------- 1 | window.WebViewContentPositioner = window.WebViewContentPositioner || {}; 2 | 3 | (function($){ 4 | 5 | const positionerEl = (function(){ 6 | const el = document.createElement('wvcp_caret'); 7 | el.style.display = 'inline-block'; 8 | el.style.height = '1px'; 9 | el.style.width = '0'; 10 | return el; 11 | })(); 12 | 13 | // XPath methods copied from https://github.com/firebug/firebug 14 | function getElementXPath(element) { 15 | if (element && element.id) 16 | return '//*[@id="' + element.id + '"]'; 17 | 18 | return getElementTreeXPath(element); 19 | } 20 | 21 | function getElementTreeXPath(element) { 22 | const paths = []; 23 | for (; element && element.nodeType == Node.ELEMENT_NODE; element = element.parentNode) { 24 | var index = 0; 25 | for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) { 26 | if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE) 27 | continue; 28 | 29 | if (sibling.nodeName == element.nodeName) 30 | ++index; 31 | } 32 | 33 | const tagName = element.nodeName.toLowerCase(); 34 | const pathIndex = (index ? "[" + (index+1) + "]" : ""); 35 | paths.splice(0, 0, tagName + pathIndex); 36 | } 37 | 38 | return paths.length ? "/" + paths.join("/") : null; 39 | } 40 | 41 | function getElementByXPath(xpath) { 42 | try { 43 | var result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null); 44 | return result.iterateNext(); 45 | } catch (exc) { 46 | return null; 47 | } 48 | } 49 | 50 | function insertPositionerToRange(range) { 51 | const startNode = range.startContainer; 52 | var parentNode, beforeNode; 53 | if (startNode.nodeType == Node.TEXT_NODE) { 54 | parentNode = startNode.parentNode; 55 | beforeNode = startNode.splitText(range.startOffset); 56 | } else { 57 | parentNode = startNode; 58 | beforeNode = startNode.firstChild; 59 | } 60 | 61 | parentNode.insertBefore(positionerEl, beforeNode); 62 | } 63 | 64 | function removePositioner() { 65 | const parent = positionerEl.parentNode; 66 | if (!parent) return; 67 | parent.removeChild(positionerEl); 68 | parent.normalize(); 69 | } 70 | 71 | function encodeRange(range) { 72 | const obj = {}; 73 | const getContainerXPath = function(c) { 74 | var textNodePath = ""; 75 | var el = c; 76 | if (c.nodeType == Node.TEXT_NODE) { 77 | el = c.parentNode; 78 | for (var i=0, cns = el.childNodes, len=cns.length; i 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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /WebViewContentPositionerTests/WebViewContentPositionerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewContentPositionerTests.swift 3 | // WebViewContentPositionerTests 4 | // 5 | // Created by CHEN Xian’an on 1/6/16. 6 | // Copyright © 2016 lazyapps. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import WebViewContentPositioner 11 | 12 | class WebViewContentPositionerTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | --------------------------------------------------------------------------------