├── .gitignore ├── .travis.yml ├── Example ├── HLLOfflineWebVC.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── HLLOfflineWebVC-Example.xcscheme ├── HLLOfflineWebVC.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── HLLOfflineWebVC │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── HLLActionSheet.h │ ├── HLLActionSheet.m │ ├── HLLAppDelegate.h │ ├── HLLAppDelegate.m │ ├── HLLBisnessWebVC.h │ ├── HLLBisnessWebVC.m │ ├── HLLIconButton.h │ ├── HLLIconButton.m │ ├── HLLOfflineWebVC-Info.plist │ ├── HLLOfflineWebVC-Prefix.pch │ ├── HLLViewController.h │ ├── HLLViewController.m │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── LocalServerTestController.h │ ├── LocalServerTestController.m │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── main.m │ └── resource │ │ ├── act3-offline-package-test.zip │ │ ├── background.png │ │ ├── close.png │ │ ├── close1.png │ │ ├── logo.png │ │ ├── name.png │ │ ├── navigation.png │ │ ├── split.png │ │ ├── up.png │ │ └── user.png ├── Podfile └── Tests │ ├── Tests-Info.plist │ ├── Tests-Prefix.pch │ ├── Tests.m │ └── en.lproj │ └── InfoPlist.strings ├── HLLOfflineWebVC.podspec ├── HLLOfflineWebVC ├── Assets │ └── .gitkeep └── Classes │ ├── .gitkeep │ ├── BaseWebViewController.h │ ├── BaseWebViewController.m │ ├── HLLOfflineWebConfig.h │ ├── HLLOfflineWebConfig.m │ ├── HLLOfflineWebPackageKit.h │ ├── HLLOfflineWebVC.h │ ├── HLLOfflineWebVC.m │ ├── OfflineWebBisNameMatch │ ├── HLLOfflineWebBisNameMatch.h │ └── HLLOfflineWebBisNameMatch.m │ ├── OfflineWebConst │ └── HLLOfflineWebConst.h │ ├── OfflineWebDevTool │ ├── HLLOfflineWebDevTool.h │ └── HLLOfflineWebDevTool.m │ ├── OfflineWebPackage │ ├── HLLOfflineWebDataReport.h │ ├── HLLOfflineWebDataReport.m │ ├── HLLOfflineWebPackage.h │ └── HLLOfflineWebPackage.m │ ├── OfflineWebUtils │ ├── HLLOfflineWebFileUtil.h │ └── HLLOfflineWebFileUtil.m │ └── Private │ ├── HLLOfflineWebDownloadMgr.h │ ├── HLLOfflineWebDownloadMgr.m │ ├── HLLOfflineWebFileMgr.h │ ├── HLLOfflineWebFileMgr.m │ └── HLLOfflineWebPackage+callbacks.h ├── Image ├── 1.gif ├── 1.jpg ├── 2.gif ├── 2.png ├── 3.png └── title.png ├── LICENSE ├── README.md ├── README_CN.md └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | */Podfile.lock 22 | 23 | # Bundler 24 | .bundle 25 | 26 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 27 | # Carthage/Checkouts 28 | 29 | Carthage/Build 30 | 31 | # We recommend against adding the Pods directory to your .gitignore. However 32 | # you should judge for yourself, the pros and cons are mentioned at: 33 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 34 | # 35 | # Note: if you ignore the Pods directory, make sure to uncomment 36 | # `pod install` in .travis.yml 37 | # 38 | # Pods/ 39 | Example/Pods 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/HLLOfflineWebVC.xcworkspace -scheme HLLOfflineWebVC-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 11 | 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; 12 | 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 13 | 6003F598195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F596195388D20070C39A /* InfoPlist.strings */; }; 14 | 6003F59A195388D20070C39A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F599195388D20070C39A /* main.m */; }; 15 | 6003F59E195388D20070C39A /* HLLAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F59D195388D20070C39A /* HLLAppDelegate.m */; }; 16 | 6003F5A7195388D20070C39A /* HLLViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5A6195388D20070C39A /* HLLViewController.m */; }; 17 | 6003F5A9195388D20070C39A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5A8195388D20070C39A /* Images.xcassets */; }; 18 | 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; 19 | 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 20 | 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 21 | 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; 22 | 6003F5BC195388D20070C39A /* Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5BB195388D20070C39A /* Tests.m */; }; 23 | 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; 24 | 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; 25 | A82101F927E1D77C001C5B0F /* LocalServerTestController.m in Sources */ = {isa = PBXBuildFile; fileRef = A82101F827E1D77C001C5B0F /* LocalServerTestController.m */; }; 26 | A891E793275B4F89000C589A /* HLLBisnessWebVC.m in Sources */ = {isa = PBXBuildFile; fileRef = A891E792275B4F89000C589A /* HLLBisnessWebVC.m */; }; 27 | A8C01D5C27E2D3280078C62B /* resource in Resources */ = {isa = PBXBuildFile; fileRef = A8C01D5B27E2D3280078C62B /* resource */; }; 28 | A8C147B9282BA2DE009379E6 /* HLLIconButton.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C147B8282BA2DE009379E6 /* HLLIconButton.m */; }; 29 | A8C147BC282BDB6C009379E6 /* HLLActionSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = A8C147BB282BDB6C009379E6 /* HLLActionSheet.m */; }; 30 | B3AD863A0B7AC90AA3DDB94A /* libPods-HLLOfflineWebVC_Example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 036076B0BF698F63C7C81EDA /* libPods-HLLOfflineWebVC_Example.a */; }; 31 | C19883AA861A1120F3FB57F8 /* libPods-HLLOfflineWebVC_Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 898F2FA39F26CC67E0B87200 /* libPods-HLLOfflineWebVC_Tests.a */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 6003F5B3195388D20070C39A /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 6003F582195388D10070C39A /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 6003F589195388D20070C39A; 40 | remoteInfo = HLLOfflineWebVC; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 036076B0BF698F63C7C81EDA /* libPods-HLLOfflineWebVC_Example.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HLLOfflineWebVC_Example.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 16508ADF1DFE2D9C9AB7A006 /* Pods-HLLOfflineWebVC_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HLLOfflineWebVC_Example.release.xcconfig"; path = "Target Support Files/Pods-HLLOfflineWebVC_Example/Pods-HLLOfflineWebVC_Example.release.xcconfig"; sourceTree = ""; }; 47 | 2AAF5EA4C84571D695063064 /* Pods-HLLOfflineWebVC_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HLLOfflineWebVC_Example.debug.xcconfig"; path = "Target Support Files/Pods-HLLOfflineWebVC_Example/Pods-HLLOfflineWebVC_Example.debug.xcconfig"; sourceTree = ""; }; 48 | 6003F58A195388D20070C39A /* HLLOfflineWebVC_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HLLOfflineWebVC_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 50 | 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 51 | 6003F591195388D20070C39A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 52 | 6003F595195388D20070C39A /* HLLOfflineWebVC-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "HLLOfflineWebVC-Info.plist"; sourceTree = ""; }; 53 | 6003F597195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 54 | 6003F599195388D20070C39A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 55 | 6003F59B195388D20070C39A /* HLLOfflineWebVC-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "HLLOfflineWebVC-Prefix.pch"; sourceTree = ""; }; 56 | 6003F59C195388D20070C39A /* HLLAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HLLAppDelegate.h; sourceTree = ""; }; 57 | 6003F59D195388D20070C39A /* HLLAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HLLAppDelegate.m; sourceTree = ""; }; 58 | 6003F5A5195388D20070C39A /* HLLViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HLLViewController.h; sourceTree = ""; }; 59 | 6003F5A6195388D20070C39A /* HLLViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HLLViewController.m; sourceTree = ""; }; 60 | 6003F5A8195388D20070C39A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 61 | 6003F5AE195388D20070C39A /* HLLOfflineWebVC_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HLLOfflineWebVC_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 63 | 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; 64 | 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 65 | 6003F5BB195388D20070C39A /* Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Tests.m; sourceTree = ""; }; 66 | 606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = ""; }; 67 | 6873E3303E7F77C5D8D09AFB /* HLLOfflineWebVC.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = HLLOfflineWebVC.podspec; path = ../HLLOfflineWebVC.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 68 | 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 69 | 759922627A9E9C8AF7C3A42E /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 70 | 859D9476A30141348498C5BD /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 71 | 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 72 | 898F2FA39F26CC67E0B87200 /* libPods-HLLOfflineWebVC_Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HLLOfflineWebVC_Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 73 | A82101F727E1D77C001C5B0F /* LocalServerTestController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalServerTestController.h; sourceTree = ""; }; 74 | A82101F827E1D77C001C5B0F /* LocalServerTestController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalServerTestController.m; sourceTree = ""; }; 75 | A891E791275B4F89000C589A /* HLLBisnessWebVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HLLBisnessWebVC.h; sourceTree = ""; }; 76 | A891E792275B4F89000C589A /* HLLBisnessWebVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HLLBisnessWebVC.m; sourceTree = ""; }; 77 | A8C01D5B27E2D3280078C62B /* resource */ = {isa = PBXFileReference; lastKnownFileType = folder; name = resource; path = HLLOfflineWebVC/resource; sourceTree = ""; }; 78 | A8C147B7282BA2DE009379E6 /* HLLIconButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HLLIconButton.h; sourceTree = ""; }; 79 | A8C147B8282BA2DE009379E6 /* HLLIconButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HLLIconButton.m; sourceTree = ""; }; 80 | A8C147BA282BDB6C009379E6 /* HLLActionSheet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HLLActionSheet.h; sourceTree = ""; }; 81 | A8C147BB282BDB6C009379E6 /* HLLActionSheet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HLLActionSheet.m; sourceTree = ""; }; 82 | D825C9F948A11C18C1B3B536 /* Pods-HLLOfflineWebVC_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HLLOfflineWebVC_Tests.release.xcconfig"; path = "Target Support Files/Pods-HLLOfflineWebVC_Tests/Pods-HLLOfflineWebVC_Tests.release.xcconfig"; sourceTree = ""; }; 83 | F462CF90AC854ABDD83228A9 /* Pods-HLLOfflineWebVC_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HLLOfflineWebVC_Tests.debug.xcconfig"; path = "Target Support Files/Pods-HLLOfflineWebVC_Tests/Pods-HLLOfflineWebVC_Tests.debug.xcconfig"; sourceTree = ""; }; 84 | /* End PBXFileReference section */ 85 | 86 | /* Begin PBXFrameworksBuildPhase section */ 87 | 6003F587195388D20070C39A /* Frameworks */ = { 88 | isa = PBXFrameworksBuildPhase; 89 | buildActionMask = 2147483647; 90 | files = ( 91 | 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */, 92 | 6003F592195388D20070C39A /* UIKit.framework in Frameworks */, 93 | 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */, 94 | B3AD863A0B7AC90AA3DDB94A /* libPods-HLLOfflineWebVC_Example.a in Frameworks */, 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | 6003F5AB195388D20070C39A /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */, 103 | 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */, 104 | 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */, 105 | C19883AA861A1120F3FB57F8 /* libPods-HLLOfflineWebVC_Tests.a in Frameworks */, 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | /* End PBXFrameworksBuildPhase section */ 110 | 111 | /* Begin PBXGroup section */ 112 | 2CE64768650F38B3B618EDB0 /* Pods */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 2AAF5EA4C84571D695063064 /* Pods-HLLOfflineWebVC_Example.debug.xcconfig */, 116 | 16508ADF1DFE2D9C9AB7A006 /* Pods-HLLOfflineWebVC_Example.release.xcconfig */, 117 | F462CF90AC854ABDD83228A9 /* Pods-HLLOfflineWebVC_Tests.debug.xcconfig */, 118 | D825C9F948A11C18C1B3B536 /* Pods-HLLOfflineWebVC_Tests.release.xcconfig */, 119 | ); 120 | path = Pods; 121 | sourceTree = ""; 122 | }; 123 | 6003F581195388D10070C39A = { 124 | isa = PBXGroup; 125 | children = ( 126 | A8C01D5B27E2D3280078C62B /* resource */, 127 | 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */, 128 | 6003F593195388D20070C39A /* Example for HLLOfflineWebVC */, 129 | 6003F5B5195388D20070C39A /* Tests */, 130 | 6003F58C195388D20070C39A /* Frameworks */, 131 | 6003F58B195388D20070C39A /* Products */, 132 | 2CE64768650F38B3B618EDB0 /* Pods */, 133 | ); 134 | sourceTree = ""; 135 | }; 136 | 6003F58B195388D20070C39A /* Products */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 6003F58A195388D20070C39A /* HLLOfflineWebVC_Example.app */, 140 | 6003F5AE195388D20070C39A /* HLLOfflineWebVC_Tests.xctest */, 141 | ); 142 | name = Products; 143 | sourceTree = ""; 144 | }; 145 | 6003F58C195388D20070C39A /* Frameworks */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 6003F58D195388D20070C39A /* Foundation.framework */, 149 | 6003F58F195388D20070C39A /* CoreGraphics.framework */, 150 | 6003F591195388D20070C39A /* UIKit.framework */, 151 | 6003F5AF195388D20070C39A /* XCTest.framework */, 152 | 036076B0BF698F63C7C81EDA /* libPods-HLLOfflineWebVC_Example.a */, 153 | 898F2FA39F26CC67E0B87200 /* libPods-HLLOfflineWebVC_Tests.a */, 154 | ); 155 | name = Frameworks; 156 | sourceTree = ""; 157 | }; 158 | 6003F593195388D20070C39A /* Example for HLLOfflineWebVC */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 6003F59C195388D20070C39A /* HLLAppDelegate.h */, 162 | 6003F59D195388D20070C39A /* HLLAppDelegate.m */, 163 | 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */, 164 | 6003F5A5195388D20070C39A /* HLLViewController.h */, 165 | 6003F5A6195388D20070C39A /* HLLViewController.m */, 166 | 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */, 167 | 6003F5A8195388D20070C39A /* Images.xcassets */, 168 | 6003F594195388D20070C39A /* Supporting Files */, 169 | A891E791275B4F89000C589A /* HLLBisnessWebVC.h */, 170 | A891E792275B4F89000C589A /* HLLBisnessWebVC.m */, 171 | A82101F727E1D77C001C5B0F /* LocalServerTestController.h */, 172 | A82101F827E1D77C001C5B0F /* LocalServerTestController.m */, 173 | A8C147B7282BA2DE009379E6 /* HLLIconButton.h */, 174 | A8C147B8282BA2DE009379E6 /* HLLIconButton.m */, 175 | A8C147BA282BDB6C009379E6 /* HLLActionSheet.h */, 176 | A8C147BB282BDB6C009379E6 /* HLLActionSheet.m */, 177 | ); 178 | name = "Example for HLLOfflineWebVC"; 179 | path = HLLOfflineWebVC; 180 | sourceTree = ""; 181 | }; 182 | 6003F594195388D20070C39A /* Supporting Files */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | 6003F595195388D20070C39A /* HLLOfflineWebVC-Info.plist */, 186 | 6003F596195388D20070C39A /* InfoPlist.strings */, 187 | 6003F599195388D20070C39A /* main.m */, 188 | 6003F59B195388D20070C39A /* HLLOfflineWebVC-Prefix.pch */, 189 | ); 190 | name = "Supporting Files"; 191 | sourceTree = ""; 192 | }; 193 | 6003F5B5195388D20070C39A /* Tests */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 6003F5BB195388D20070C39A /* Tests.m */, 197 | 6003F5B6195388D20070C39A /* Supporting Files */, 198 | ); 199 | path = Tests; 200 | sourceTree = ""; 201 | }; 202 | 6003F5B6195388D20070C39A /* Supporting Files */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 6003F5B7195388D20070C39A /* Tests-Info.plist */, 206 | 6003F5B8195388D20070C39A /* InfoPlist.strings */, 207 | 606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */, 208 | ); 209 | name = "Supporting Files"; 210 | sourceTree = ""; 211 | }; 212 | 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 6873E3303E7F77C5D8D09AFB /* HLLOfflineWebVC.podspec */, 216 | 859D9476A30141348498C5BD /* README.md */, 217 | 759922627A9E9C8AF7C3A42E /* LICENSE */, 218 | ); 219 | name = "Podspec Metadata"; 220 | sourceTree = ""; 221 | }; 222 | /* End PBXGroup section */ 223 | 224 | /* Begin PBXNativeTarget section */ 225 | 6003F589195388D20070C39A /* HLLOfflineWebVC_Example */ = { 226 | isa = PBXNativeTarget; 227 | buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "HLLOfflineWebVC_Example" */; 228 | buildPhases = ( 229 | 6C4365C19F1ACB930A0653AA /* [CP] Check Pods Manifest.lock */, 230 | 6003F586195388D20070C39A /* Sources */, 231 | 6003F587195388D20070C39A /* Frameworks */, 232 | 6003F588195388D20070C39A /* Resources */, 233 | ); 234 | buildRules = ( 235 | ); 236 | dependencies = ( 237 | ); 238 | name = HLLOfflineWebVC_Example; 239 | productName = HLLOfflineWebVC; 240 | productReference = 6003F58A195388D20070C39A /* HLLOfflineWebVC_Example.app */; 241 | productType = "com.apple.product-type.application"; 242 | }; 243 | 6003F5AD195388D20070C39A /* HLLOfflineWebVC_Tests */ = { 244 | isa = PBXNativeTarget; 245 | buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "HLLOfflineWebVC_Tests" */; 246 | buildPhases = ( 247 | 08118279C1234F841EC3A80A /* [CP] Check Pods Manifest.lock */, 248 | 6003F5AA195388D20070C39A /* Sources */, 249 | 6003F5AB195388D20070C39A /* Frameworks */, 250 | 6003F5AC195388D20070C39A /* Resources */, 251 | ); 252 | buildRules = ( 253 | ); 254 | dependencies = ( 255 | 6003F5B4195388D20070C39A /* PBXTargetDependency */, 256 | ); 257 | name = HLLOfflineWebVC_Tests; 258 | productName = HLLOfflineWebVCTests; 259 | productReference = 6003F5AE195388D20070C39A /* HLLOfflineWebVC_Tests.xctest */; 260 | productType = "com.apple.product-type.bundle.unit-test"; 261 | }; 262 | /* End PBXNativeTarget section */ 263 | 264 | /* Begin PBXProject section */ 265 | 6003F582195388D10070C39A /* Project object */ = { 266 | isa = PBXProject; 267 | attributes = { 268 | CLASSPREFIX = HLL; 269 | KnownAssetTags = ( 270 | New, 271 | ); 272 | LastUpgradeCheck = 0720; 273 | ORGANIZATIONNAME = been.han; 274 | TargetAttributes = { 275 | 6003F589195388D20070C39A = { 276 | DevelopmentTeam = 42N88G8G7H; 277 | }; 278 | 6003F5AD195388D20070C39A = { 279 | TestTargetID = 6003F589195388D20070C39A; 280 | }; 281 | }; 282 | }; 283 | buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "HLLOfflineWebVC" */; 284 | compatibilityVersion = "Xcode 3.2"; 285 | developmentRegion = English; 286 | hasScannedForEncodings = 0; 287 | knownRegions = ( 288 | English, 289 | en, 290 | Base, 291 | ); 292 | mainGroup = 6003F581195388D10070C39A; 293 | productRefGroup = 6003F58B195388D20070C39A /* Products */; 294 | projectDirPath = ""; 295 | projectRoot = ""; 296 | targets = ( 297 | 6003F589195388D20070C39A /* HLLOfflineWebVC_Example */, 298 | 6003F5AD195388D20070C39A /* HLLOfflineWebVC_Tests */, 299 | ); 300 | }; 301 | /* End PBXProject section */ 302 | 303 | /* Begin PBXResourcesBuildPhase section */ 304 | 6003F588195388D20070C39A /* Resources */ = { 305 | isa = PBXResourcesBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | A8C01D5C27E2D3280078C62B /* resource in Resources */, 309 | 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */, 310 | 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */, 311 | 6003F5A9195388D20070C39A /* Images.xcassets in Resources */, 312 | 6003F598195388D20070C39A /* InfoPlist.strings in Resources */, 313 | ); 314 | runOnlyForDeploymentPostprocessing = 0; 315 | }; 316 | 6003F5AC195388D20070C39A /* Resources */ = { 317 | isa = PBXResourcesBuildPhase; 318 | buildActionMask = 2147483647; 319 | files = ( 320 | 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */, 321 | ); 322 | runOnlyForDeploymentPostprocessing = 0; 323 | }; 324 | /* End PBXResourcesBuildPhase section */ 325 | 326 | /* Begin PBXShellScriptBuildPhase section */ 327 | 08118279C1234F841EC3A80A /* [CP] Check Pods Manifest.lock */ = { 328 | isa = PBXShellScriptBuildPhase; 329 | buildActionMask = 2147483647; 330 | files = ( 331 | ); 332 | inputFileListPaths = ( 333 | ); 334 | inputPaths = ( 335 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 336 | "${PODS_ROOT}/Manifest.lock", 337 | ); 338 | name = "[CP] Check Pods Manifest.lock"; 339 | outputFileListPaths = ( 340 | ); 341 | outputPaths = ( 342 | "$(DERIVED_FILE_DIR)/Pods-HLLOfflineWebVC_Tests-checkManifestLockResult.txt", 343 | ); 344 | runOnlyForDeploymentPostprocessing = 0; 345 | shellPath = /bin/sh; 346 | 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# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 347 | showEnvVarsInLog = 0; 348 | }; 349 | 6C4365C19F1ACB930A0653AA /* [CP] Check Pods Manifest.lock */ = { 350 | isa = PBXShellScriptBuildPhase; 351 | buildActionMask = 2147483647; 352 | files = ( 353 | ); 354 | inputFileListPaths = ( 355 | ); 356 | inputPaths = ( 357 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 358 | "${PODS_ROOT}/Manifest.lock", 359 | ); 360 | name = "[CP] Check Pods Manifest.lock"; 361 | outputFileListPaths = ( 362 | ); 363 | outputPaths = ( 364 | "$(DERIVED_FILE_DIR)/Pods-HLLOfflineWebVC_Example-checkManifestLockResult.txt", 365 | ); 366 | runOnlyForDeploymentPostprocessing = 0; 367 | shellPath = /bin/sh; 368 | 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# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 369 | showEnvVarsInLog = 0; 370 | }; 371 | /* End PBXShellScriptBuildPhase section */ 372 | 373 | /* Begin PBXSourcesBuildPhase section */ 374 | 6003F586195388D20070C39A /* Sources */ = { 375 | isa = PBXSourcesBuildPhase; 376 | buildActionMask = 2147483647; 377 | files = ( 378 | A891E793275B4F89000C589A /* HLLBisnessWebVC.m in Sources */, 379 | 6003F59E195388D20070C39A /* HLLAppDelegate.m in Sources */, 380 | A82101F927E1D77C001C5B0F /* LocalServerTestController.m in Sources */, 381 | A8C147B9282BA2DE009379E6 /* HLLIconButton.m in Sources */, 382 | 6003F5A7195388D20070C39A /* HLLViewController.m in Sources */, 383 | A8C147BC282BDB6C009379E6 /* HLLActionSheet.m in Sources */, 384 | 6003F59A195388D20070C39A /* main.m in Sources */, 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | }; 388 | 6003F5AA195388D20070C39A /* Sources */ = { 389 | isa = PBXSourcesBuildPhase; 390 | buildActionMask = 2147483647; 391 | files = ( 392 | 6003F5BC195388D20070C39A /* Tests.m in Sources */, 393 | ); 394 | runOnlyForDeploymentPostprocessing = 0; 395 | }; 396 | /* End PBXSourcesBuildPhase section */ 397 | 398 | /* Begin PBXTargetDependency section */ 399 | 6003F5B4195388D20070C39A /* PBXTargetDependency */ = { 400 | isa = PBXTargetDependency; 401 | target = 6003F589195388D20070C39A /* HLLOfflineWebVC_Example */; 402 | targetProxy = 6003F5B3195388D20070C39A /* PBXContainerItemProxy */; 403 | }; 404 | /* End PBXTargetDependency section */ 405 | 406 | /* Begin PBXVariantGroup section */ 407 | 6003F596195388D20070C39A /* InfoPlist.strings */ = { 408 | isa = PBXVariantGroup; 409 | children = ( 410 | 6003F597195388D20070C39A /* en */, 411 | ); 412 | name = InfoPlist.strings; 413 | sourceTree = ""; 414 | }; 415 | 6003F5B8195388D20070C39A /* InfoPlist.strings */ = { 416 | isa = PBXVariantGroup; 417 | children = ( 418 | 6003F5B9195388D20070C39A /* en */, 419 | ); 420 | name = InfoPlist.strings; 421 | sourceTree = ""; 422 | }; 423 | 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */ = { 424 | isa = PBXVariantGroup; 425 | children = ( 426 | 71719F9E1E33DC2100824A3D /* Base */, 427 | ); 428 | name = LaunchScreen.storyboard; 429 | sourceTree = ""; 430 | }; 431 | /* End PBXVariantGroup section */ 432 | 433 | /* Begin XCBuildConfiguration section */ 434 | 6003F5BD195388D20070C39A /* Debug */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | ALWAYS_SEARCH_USER_PATHS = NO; 438 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 439 | CLANG_CXX_LIBRARY = "libc++"; 440 | CLANG_ENABLE_MODULES = YES; 441 | CLANG_ENABLE_OBJC_ARC = YES; 442 | CLANG_WARN_BOOL_CONVERSION = YES; 443 | CLANG_WARN_CONSTANT_CONVERSION = YES; 444 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 445 | CLANG_WARN_EMPTY_BODY = YES; 446 | CLANG_WARN_ENUM_CONVERSION = YES; 447 | CLANG_WARN_INT_CONVERSION = YES; 448 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 449 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 450 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 451 | COPY_PHASE_STRIP = NO; 452 | ENABLE_BITCODE = NO; 453 | ENABLE_TESTABILITY = YES; 454 | GCC_C_LANGUAGE_STANDARD = gnu99; 455 | GCC_DYNAMIC_NO_PIC = NO; 456 | GCC_OPTIMIZATION_LEVEL = 0; 457 | GCC_PREPROCESSOR_DEFINITIONS = ( 458 | "DEBUG=1", 459 | "$(inherited)", 460 | ); 461 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 462 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 463 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 464 | GCC_WARN_UNDECLARED_SELECTOR = YES; 465 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 466 | GCC_WARN_UNUSED_FUNCTION = YES; 467 | GCC_WARN_UNUSED_VARIABLE = YES; 468 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 469 | ONLY_ACTIVE_ARCH = YES; 470 | SDKROOT = iphoneos; 471 | TARGETED_DEVICE_FAMILY = "1,2"; 472 | }; 473 | name = Debug; 474 | }; 475 | 6003F5BE195388D20070C39A /* Release */ = { 476 | isa = XCBuildConfiguration; 477 | buildSettings = { 478 | ALWAYS_SEARCH_USER_PATHS = NO; 479 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 480 | CLANG_CXX_LIBRARY = "libc++"; 481 | CLANG_ENABLE_MODULES = YES; 482 | CLANG_ENABLE_OBJC_ARC = YES; 483 | CLANG_WARN_BOOL_CONVERSION = YES; 484 | CLANG_WARN_CONSTANT_CONVERSION = YES; 485 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 486 | CLANG_WARN_EMPTY_BODY = YES; 487 | CLANG_WARN_ENUM_CONVERSION = YES; 488 | CLANG_WARN_INT_CONVERSION = YES; 489 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 490 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 491 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 492 | COPY_PHASE_STRIP = YES; 493 | ENABLE_BITCODE = NO; 494 | ENABLE_NS_ASSERTIONS = NO; 495 | GCC_C_LANGUAGE_STANDARD = gnu99; 496 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 497 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 498 | GCC_WARN_UNDECLARED_SELECTOR = YES; 499 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 500 | GCC_WARN_UNUSED_FUNCTION = YES; 501 | GCC_WARN_UNUSED_VARIABLE = YES; 502 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 503 | SDKROOT = iphoneos; 504 | TARGETED_DEVICE_FAMILY = "1,2"; 505 | VALIDATE_PRODUCT = YES; 506 | }; 507 | name = Release; 508 | }; 509 | 6003F5C0195388D20070C39A /* Debug */ = { 510 | isa = XCBuildConfiguration; 511 | baseConfigurationReference = 2AAF5EA4C84571D695063064 /* Pods-HLLOfflineWebVC_Example.debug.xcconfig */; 512 | buildSettings = { 513 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 514 | DEVELOPMENT_TEAM = 42N88G8G7H; 515 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 516 | GCC_PREFIX_HEADER = "HLLOfflineWebVC/HLLOfflineWebVC-Prefix.pch"; 517 | INFOPLIST_FILE = "HLLOfflineWebVC/HLLOfflineWebVC-Info.plist"; 518 | MODULE_NAME = ExampleApp; 519 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 520 | PRODUCT_NAME = "$(TARGET_NAME)"; 521 | SWIFT_VERSION = 4.0; 522 | WRAPPER_EXTENSION = app; 523 | }; 524 | name = Debug; 525 | }; 526 | 6003F5C1195388D20070C39A /* Release */ = { 527 | isa = XCBuildConfiguration; 528 | baseConfigurationReference = 16508ADF1DFE2D9C9AB7A006 /* Pods-HLLOfflineWebVC_Example.release.xcconfig */; 529 | buildSettings = { 530 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 531 | DEVELOPMENT_TEAM = KM8A8SL9K2; 532 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 533 | GCC_PREFIX_HEADER = "HLLOfflineWebVC/HLLOfflineWebVC-Prefix.pch"; 534 | INFOPLIST_FILE = "HLLOfflineWebVC/HLLOfflineWebVC-Info.plist"; 535 | MODULE_NAME = ExampleApp; 536 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 537 | PRODUCT_NAME = "$(TARGET_NAME)"; 538 | SWIFT_VERSION = 4.0; 539 | WRAPPER_EXTENSION = app; 540 | }; 541 | name = Release; 542 | }; 543 | 6003F5C3195388D20070C39A /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | baseConfigurationReference = F462CF90AC854ABDD83228A9 /* Pods-HLLOfflineWebVC_Tests.debug.xcconfig */; 546 | buildSettings = { 547 | BUNDLE_LOADER = "$(TEST_HOST)"; 548 | FRAMEWORK_SEARCH_PATHS = ( 549 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 550 | "$(inherited)", 551 | "$(DEVELOPER_FRAMEWORKS_DIR)", 552 | ); 553 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 554 | GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; 555 | GCC_PREPROCESSOR_DEFINITIONS = ( 556 | "DEBUG=1", 557 | "$(inherited)", 558 | ); 559 | INFOPLIST_FILE = "Tests/Tests-Info.plist"; 560 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 561 | PRODUCT_NAME = "$(TARGET_NAME)"; 562 | SWIFT_VERSION = 4.0; 563 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HLLOfflineWebVC_Example.app/HLLOfflineWebVC_Example"; 564 | WRAPPER_EXTENSION = xctest; 565 | }; 566 | name = Debug; 567 | }; 568 | 6003F5C4195388D20070C39A /* Release */ = { 569 | isa = XCBuildConfiguration; 570 | baseConfigurationReference = D825C9F948A11C18C1B3B536 /* Pods-HLLOfflineWebVC_Tests.release.xcconfig */; 571 | buildSettings = { 572 | BUNDLE_LOADER = "$(TEST_HOST)"; 573 | FRAMEWORK_SEARCH_PATHS = ( 574 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 575 | "$(inherited)", 576 | "$(DEVELOPER_FRAMEWORKS_DIR)", 577 | ); 578 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 579 | GCC_PREFIX_HEADER = "Tests/Tests-Prefix.pch"; 580 | INFOPLIST_FILE = "Tests/Tests-Info.plist"; 581 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; 582 | PRODUCT_NAME = "$(TARGET_NAME)"; 583 | SWIFT_VERSION = 4.0; 584 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HLLOfflineWebVC_Example.app/HLLOfflineWebVC_Example"; 585 | WRAPPER_EXTENSION = xctest; 586 | }; 587 | name = Release; 588 | }; 589 | /* End XCBuildConfiguration section */ 590 | 591 | /* Begin XCConfigurationList section */ 592 | 6003F585195388D10070C39A /* Build configuration list for PBXProject "HLLOfflineWebVC" */ = { 593 | isa = XCConfigurationList; 594 | buildConfigurations = ( 595 | 6003F5BD195388D20070C39A /* Debug */, 596 | 6003F5BE195388D20070C39A /* Release */, 597 | ); 598 | defaultConfigurationIsVisible = 0; 599 | defaultConfigurationName = Release; 600 | }; 601 | 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "HLLOfflineWebVC_Example" */ = { 602 | isa = XCConfigurationList; 603 | buildConfigurations = ( 604 | 6003F5C0195388D20070C39A /* Debug */, 605 | 6003F5C1195388D20070C39A /* Release */, 606 | ); 607 | defaultConfigurationIsVisible = 0; 608 | defaultConfigurationName = Release; 609 | }; 610 | 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "HLLOfflineWebVC_Tests" */ = { 611 | isa = XCConfigurationList; 612 | buildConfigurations = ( 613 | 6003F5C3195388D20070C39A /* Debug */, 614 | 6003F5C4195388D20070C39A /* Release */, 615 | ); 616 | defaultConfigurationIsVisible = 0; 617 | defaultConfigurationName = Release; 618 | }; 619 | /* End XCConfigurationList section */ 620 | }; 621 | rootObject = 6003F582195388D10070C39A /* Project object */; 622 | } 623 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC.xcodeproj/xcshareddata/xcschemes/HLLOfflineWebVC-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/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 | 29 | 30 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLActionSheet.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLActionSheet.h 3 | // HLLOfflineWebVC_Example 4 | // 5 | // Created by 货拉拉 on 2022/5/11. 6 | // Copyright © 2022 货拉拉. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | ///自定义弹窗 13 | @interface HLLActionSheet : UIViewController 14 | - (instancetype)initWithTitle:(NSString *)title Content:(NSString *)content; 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLActionSheet.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLActionSheet.m 3 | // HLLOfflineWebVC_Example 4 | // 5 | // Created by 货拉拉 on 2022/5/11. 6 | // Copyright © 2022 货拉拉. All rights reserved. 7 | // 8 | 9 | #import "HLLActionSheet.h" 10 | #import 11 | 12 | #define GetScreenWidth [[UIScreen mainScreen] bounds].size.width 13 | #define GetScreenHeight [[UIScreen mainScreen] bounds].size.height 14 | 15 | @interface HLLActionSheet () 16 | @property (nonatomic, copy) NSString *titleText; 17 | @property (nonatomic, copy) NSString *contentText; 18 | 19 | @end 20 | @implementation HLLActionSheet 21 | - (instancetype)initWithTitle:(NSString *)title Content:(NSString *)content { 22 | self = [super init]; 23 | if (self) { 24 | self.titleText = title; 25 | self.contentText = content; 26 | } 27 | return self; 28 | } 29 | - (void)viewDidLoad { 30 | // UIView 的部分圆角的设定 31 | UIView *mainView = [[UIView alloc] initWithFrame:CGRectMake(0, GetScreenHeight - 300, GetScreenWidth, 300)]; 32 | [self.view addSubview:mainView]; 33 | [mainView setBackgroundColor:[UIColor whiteColor]]; 34 | UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:mainView.bounds 35 | byRoundingCorners:(UIRectCornerTopRight | UIRectCornerTopLeft) 36 | cornerRadii:CGSizeMake(16, 16)]; //圆角大小 37 | CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; 38 | maskLayer.frame = mainView.bounds; 39 | maskLayer.path = maskPath.CGPath; 40 | mainView.layer.mask = maskLayer; 41 | 42 | UILabel *titleLabel = [[UILabel alloc] init]; 43 | [mainView addSubview:titleLabel]; 44 | [titleLabel setText:self.titleText]; 45 | titleLabel.textAlignment = NSTextAlignmentCenter; 46 | [titleLabel setFont:[UIFont fontWithName:@"PingFangSC-Regular" size:16]]; 47 | [titleLabel setTextColor:[UIColor blackColor]]; 48 | [titleLabel mas_remakeConstraints:^(MASConstraintMaker *make) { 49 | make.top.mas_equalTo(mainView).offset(11); 50 | make.height.mas_equalTo(22); 51 | make.left.equalTo(self.view).offset(20); 52 | make.right.equalTo(self.view).offset(-20); 53 | }]; 54 | 55 | UILabel *contentLabel = [[UILabel alloc] init]; 56 | [mainView addSubview:contentLabel]; 57 | [contentLabel setText:self.contentText]; 58 | [contentLabel setTextColor:[UIColor blackColor]]; 59 | contentLabel.textAlignment = NSTextAlignmentLeft; 60 | contentLabel.lineBreakMode = NSLineBreakByWordWrapping; 61 | [contentLabel setFont:[UIFont systemFontOfSize:14]]; 62 | contentLabel.numberOfLines = 0; 63 | [contentLabel mas_remakeConstraints:^(MASConstraintMaker *make) { 64 | make.top.mas_equalTo(titleLabel.mas_bottom).offset(5); 65 | make.bottom.mas_equalTo(-50); 66 | make.left.equalTo(mainView).offset(20); 67 | make.right.equalTo(mainView).offset(-20); 68 | }]; 69 | 70 | UIButton *iconCloseButton = [[UIButton alloc] initWithFrame:CGRectMake(19.33, 15.33, 13.3, 13.3)]; 71 | [iconCloseButton setImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"resource/close1" 72 | ofType:@"png"]] 73 | forState:UIControlStateNormal]; 74 | [mainView addSubview:iconCloseButton]; 75 | [iconCloseButton addTarget:self action:@selector(closeBtnClick:) forControlEvents:UIControlEventTouchUpInside]; 76 | 77 | UIButton *textcloseButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 13.3, 13.3)]; 78 | [textcloseButton setTitle:@"关闭" forState:UIControlStateNormal]; 79 | [textcloseButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 80 | textcloseButton.titleLabel.textAlignment = NSTextAlignmentCenter; 81 | [textcloseButton.titleLabel setFont:[UIFont systemFontOfSize:16]]; 82 | textcloseButton.layer.borderWidth = 1; 83 | textcloseButton.layer.cornerRadius = 8; 84 | textcloseButton.layer.borderColor = 85 | [UIColor colorWithRed:216.0 / 255 green:222.0 / 255 blue:235.0 / 255 alpha:1].CGColor; 86 | [mainView addSubview:textcloseButton]; 87 | [textcloseButton addTarget:self action:@selector(closeBtnClick:) forControlEvents:UIControlEventTouchUpInside]; 88 | [textcloseButton mas_remakeConstraints:^(MASConstraintMaker *make) { 89 | make.bottom.mas_equalTo(mainView.mas_bottom).offset(-34); 90 | make.height.mas_equalTo(48); 91 | make.left.equalTo(mainView).offset(16); 92 | make.right.equalTo(mainView).offset(-16); 93 | }]; 94 | } 95 | 96 | - (void)closeBtnClick:(UIButton *)button { 97 | [self dismissViewControllerAnimated:YES completion:nil]; 98 | } 99 | @end 100 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLAppDelegate.h 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 11/02/2021. 6 | // Copyright (c) 2021 货拉拉. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | 11 | @interface HLLAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLAppDelegate.m 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 11/02/2021. 6 | // Copyright (c) 2021 货拉拉. All rights reserved. 7 | // 8 | 9 | #import "HLLAppDelegate.h" 10 | #import "HLLViewController.h" 11 | @implementation HLLAppDelegate 12 | 13 | 14 | - (NSString *)getEnvAbsSuffix { 15 | return @""; 16 | } 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | self.window=[[UIWindow alloc]init]; 21 | UINavigationController *naviVC=[[UINavigationController alloc]initWithRootViewController:[[HLLViewController alloc]init]]; 22 | self.window.rootViewController=naviVC; 23 | return YES; 24 | } 25 | 26 | - (void)applicationWillResignActive:(UIApplication *)application { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of 28 | // temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application 29 | // and it begins the transition to the background state. Use this method to pause ongoing tasks, disable timers, and 30 | // throttle down OpenGL ES frame rates. Games should use this method to pause the game. 31 | } 32 | 33 | - (void)applicationDidEnterBackground:(UIApplication *)application { 34 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application 35 | // state information to restore your application to its current state in case it is terminated later. If your 36 | // application supports background execution, this method is called instead of applicationWillTerminate: when the 37 | // user quits. 38 | } 39 | 40 | - (void)applicationWillEnterForeground:(UIApplication *)application { 41 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes 42 | // made on entering the background. 43 | } 44 | 45 | - (void)applicationDidBecomeActive:(UIApplication *)application { 46 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application 47 | // was previously in the background, optionally refresh the user interface. 48 | } 49 | 50 | - (void)applicationWillTerminate:(UIApplication *)application { 51 | // Called when the application is about to terminate. Save data if appropriate. See also 52 | // applicationDidEnterBackground:. 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLBisnessWebVC.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLTestWebVC.h 3 | // HLLOfflineWebVC_Example 4 | // 5 | // Created by 货拉拉 on 2021/12/4. 6 | // Copyright © 2021 货拉拉. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface HLLBisnessWebVC : HLLOfflineWebVC 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLBisnessWebVC.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLTestWebVC.m 3 | // HLLOfflineWebVC_Example 4 | // 5 | // Created by 货拉拉 on 2021/12/4. 6 | // Copyright © 2021 货拉拉. All rights reserved. 7 | // 8 | 9 | #import "HLLBisnessWebVC.h" 10 | 11 | @implementation HLLBisnessWebVC 12 | 13 | - (void)dealloc { 14 | NSLog(@"dealloc - %@", NSStringFromClass(self.class)); 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLIconButton.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLIconButton.h 3 | // HLLOfflineWebVC_Example 4 | // 5 | // Created by 货拉拉 on 2022/5/11. 6 | // Copyright © 2022 货拉拉. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | ///自定义按钮 13 | @interface HLLIconButton : UIButton 14 | - (instancetype)initWithFrame:(CGRect)frame Icon:(UIImage *)image Text:(NSString *)text; 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLIconButton.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLIconButton.m 3 | // HLLOfflineWebVC_Example 4 | // 5 | // Created by 货拉拉 on 2022/5/11. 6 | // Copyright © 2022 货拉拉. All rights reserved. 7 | // 8 | 9 | #import "HLLIconButton.h" 10 | @implementation HLLIconButton 11 | - (instancetype)initWithFrame:(CGRect)frame Icon:(UIImage *)image Text:(NSString *)text { 12 | self = [super initWithFrame:frame]; 13 | if (self) { 14 | } 15 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(24, 16, 16, 16)]; 16 | [imageView setImage:image]; 17 | [self addSubview:imageView]; 18 | 19 | UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(64, 16, 1, 16)]; 20 | [label setBackgroundColor:[UIColor colorWithWhite:1 alpha:0.5]]; 21 | [self addSubview:label]; 22 | 23 | UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(89, 13, 104, 22)]; 24 | [textLabel setText:text]; 25 | [textLabel setTextColor:[UIColor whiteColor]]; 26 | [textLabel setTextAlignment:(NSTextAlignmentLeft)]; 27 | [textLabel setFont:[UIFont systemFontOfSize:15]]; 28 | [self addSubview:textLabel]; 29 | 30 | [self setBackgroundColor:[UIColor colorWithRed:37.0 / 255 green:43.0 / 255 blue:71.0 / 255 alpha:1]]; 31 | self.layer.borderColor = [UIColor whiteColor].CGColor; 32 | self.layer.borderWidth = 1; 33 | self.layer.cornerRadius = 4; 34 | return self; 35 | } 36 | @end 37 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLOfflineWebVC-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | en 12 | CFBundleDisplayName 13 | ${PRODUCT_NAME} 14 | CFBundleExecutable 15 | ${EXECUTABLE_NAME} 16 | CFBundleIdentifier 17 | $(PRODUCT_BUNDLE_IDENTIFIER) 18 | CFBundleInfoDictionaryVersion 19 | 6.0 20 | CFBundleName 21 | ${PRODUCT_NAME} 22 | CFBundlePackageType 23 | APPL 24 | CFBundleShortVersionString 25 | 1.0 26 | CFBundleSignature 27 | ???? 28 | CFBundleVersion 29 | 1.0 30 | LSRequiresIPhoneOS 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIMainStoryboardFile 35 | Main 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UISupportedInterfaceOrientations~ipad 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationPortraitUpsideDown 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLOfflineWebVC-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #import 8 | 9 | #ifndef __IPHONE_5_0 10 | #warning "This project uses features only available in iOS SDK 5.0 and later." 11 | #endif 12 | 13 | #ifdef __OBJC__ 14 | @import UIKit; 15 | @import Foundation; 16 | #endif 17 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLViewController.h 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 11/02/2021. 6 | // Copyright (c) 2021 货拉拉. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | 11 | @interface HLLViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/HLLViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLViewController.m 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 11/02/2021. 6 | // Copyright (c) 2021 货拉拉. All rights reserved. 7 | // 8 | #import "HLLActionSheet.h" 9 | #import "HLLBisnessWebVC.h" 10 | #import "HLLIconButton.h" 11 | #import "HLLOfflineWebFileMgr.h" 12 | #import "HLLOfflineWebPackage.h" 13 | #import "HLLViewController.h" 14 | #import "LocalServerTestController.h" 15 | #import 16 | #import 17 | //默认开启离线包,并且预拉取测试离线包 18 | #define DefaultWebPackageConfig \ 19 | ({ \ 20 | NSMutableDictionary *dic = [HLLOfflineWebConfig defaultOffWebConfigDic].mutableCopy; \ 21 | dic[kHLLOfflineWebConfigKey_predownloadList] = @[ @"act3-offline-package-test" ]; \ 22 | dic[kHLLOfflineWebConfigKey_switch] = @(1); \ 23 | dic; \ 24 | }) 25 | 26 | #define GetScreenWidth [[UIScreen mainScreen] bounds].size.width 27 | #define GetScreenHeight [[UIScreen mainScreen] bounds].size.height 28 | 29 | #define TestUrl @"https://www.baidu.com/?offweb=act3-offline-package-test" 30 | 31 | @interface HLLViewController () 32 | @property (nonatomic, strong) HLLIconButton *predownloadBtn; 33 | @property (nonatomic, strong) HLLIconButton *openWebviewBtn; 34 | @property (nonatomic, strong) HLLIconButton *testgetFileURLBtn; 35 | @property (nonatomic, strong) HLLIconButton *testgetOffWebBisNameBtn; 36 | @end 37 | 38 | @implementation HLLViewController 39 | 40 | - (void)viewDidLoad { 41 | [super viewDidLoad]; 42 | LocalServerTestController *localServerVC = [[LocalServerTestController alloc] init]; 43 | [self.view addSubview:localServerVC.view]; 44 | [self addChildViewController:localServerVC]; 45 | [self initUI]; 46 | [self initOfflineWeb]; 47 | [self preDownloadOfflineWeb]; 48 | } 49 | - (void)initOfflineWeb { 50 | NSDictionary *offwebConfigDict = DefaultWebPackageConfig; 51 | //初始化离线包block 52 | [HLLOfflineWebConfig setInitalParam:offwebConfigDict 53 | logBlock:^(HLLOfflineWebLogLevel level, NSString *keyword, NSString *message) { 54 | if (keyword == nil || message == nil) { 55 | NSLog(@"offwebpack,para is nil"); 56 | return; 57 | } 58 | switch (level) { 59 | case HLLOfflineWebLogLevelError: 60 | case HLLOfflineWebLogLevelWarning: 61 | case HLLOfflineWebLogLevelInfo: 62 | case HLLOfflineWebLogLevelDebug: 63 | NSLog(@"offwebpack:%d:%@:%@", (int)level, keyword, message); 64 | break; 65 | default: 66 | break; 67 | } 68 | } 69 | reportBlock:^(NSString *event, NSDictionary *dict) { 70 | if (event == nil || dict == nil) { 71 | NSLog(@"offwebpack,parse is nil"); 72 | return; 73 | } 74 | NSLog(@"data report:%@,%@", event, dict); 75 | } 76 | monitorBlock:^(HLLOfflineWebMonitorType type, NSString *key, CGFloat value, NSDictionary *lables) { 77 | if (type == HLLOfflineWebMonitorTypeCounter) { 78 | NSLog(@"use your monitor sdk "); 79 | } else if (type == HLLOfflineWebMonitorTypeSummary) { 80 | NSLog(@"use your monitor sdk "); 81 | } 82 | } 83 | env:@"prd" 84 | appversion:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]; 85 | } 86 | 87 | - (void)preDownloadOfflineWeb { 88 | NSDictionary *offwebConfigDict = DefaultWebPackageConfig; 89 | [HLLOfflineWebConfig predownloadOffWebPackage:offwebConfigDict]; 90 | } 91 | 92 | - (void)initUI { 93 | [self.view setBackgroundColor:[UIColor colorWithRed:15.0 / 255 green:18.0 / 255 blue:41.0 / 255 alpha:0.75]]; 94 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, GetScreenWidth, GetScreenHeight)]; 95 | NSString *path = [[NSBundle mainBundle] pathForResource:@"resource/background" ofType:@"png"]; 96 | [imageView setImage:[UIImage imageWithContentsOfFile:path]]; 97 | [self.view addSubview:imageView]; 98 | 99 | UIImageView *logoImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 64, 64)]; 100 | [logoImage setImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"resource/logo" 101 | ofType:@"png"]]]; 102 | [self.view addSubview:logoImage]; 103 | [logoImage mas_remakeConstraints:^(MASConstraintMaker *make) { 104 | make.top.mas_equalTo(self.view).offset(125 - 60); 105 | make.centerX.mas_equalTo(self.view); 106 | make.width.mas_equalTo(64.0); 107 | make.height.mas_equalTo(64.0); 108 | }]; 109 | 110 | UIImageView *nameImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 64, 64)]; 111 | [nameImage setImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"resource/name" 112 | ofType:@"png"]]]; 113 | [self.view addSubview:nameImage]; 114 | [nameImage mas_remakeConstraints:^(MASConstraintMaker *make) { 115 | make.top.mas_equalTo(logoImage.mas_bottom).offset(10); 116 | make.centerX.mas_equalTo(self.view); 117 | make.width.mas_equalTo(99.0); 118 | make.height.mas_equalTo(36.0); 119 | }]; 120 | 121 | UILabel *titleText = [[UILabel alloc] initWithFrame:CGRectMake(10, 60, GetScreenWidth - 20, 30)]; 122 | [titleText setText:@"离线包DEMO"]; 123 | [titleText setTextAlignment:NSTextAlignmentCenter]; 124 | [titleText setTextColor:[UIColor whiteColor]]; 125 | [titleText setFont:[UIFont systemFontOfSize:18]]; 126 | [self.view addSubview:titleText]; 127 | [titleText mas_remakeConstraints:^(MASConstraintMaker *make) { 128 | make.top.mas_equalTo(nameImage.mas_bottom).offset(10); 129 | make.centerX.mas_equalTo(self.view); 130 | make.width.mas_equalTo(150.0); 131 | make.height.mas_equalTo(30.0); 132 | }]; 133 | 134 | //第一行按钮 135 | self.predownloadBtn = [[HLLIconButton alloc] 136 | initWithFrame:CGRectMake(20, 500, 270, 48) 137 | Icon:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"resource/up" 138 | ofType:@"png"]] 139 | Text:@"检查更新"]; 140 | [self.view addSubview:self.predownloadBtn]; 141 | [self.predownloadBtn addTarget:self action:@selector(predownload:) forControlEvents:UIControlEventTouchUpInside]; 142 | [self.predownloadBtn mas_remakeConstraints:^(MASConstraintMaker *make) { 143 | make.bottom.mas_equalTo(self.view.mas_bottom).offset(-57 + 30); 144 | make.height.mas_equalTo(48); 145 | make.left.equalTo(self.view).offset(20); 146 | make.right.equalTo(self.view).offset(-20); 147 | }]; 148 | 149 | self.openWebviewBtn = [[HLLIconButton alloc] 150 | initWithFrame:CGRectMake(20, 500, 270, 48) 151 | Icon:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"resource/split" 152 | ofType:@"png"]] 153 | Text:@"打开webview"]; 154 | [self.openWebviewBtn addTarget:self action:@selector(openWebview:) forControlEvents:UIControlEventTouchUpInside]; 155 | [self.view addSubview:self.openWebviewBtn]; 156 | [self.openWebviewBtn mas_remakeConstraints:^(MASConstraintMaker *make) { 157 | make.bottom.mas_equalTo(self.predownloadBtn.mas_top).offset(-10); 158 | make.height.mas_equalTo(48); 159 | make.left.equalTo(self.view).offset(20); 160 | make.right.equalTo(self.view).offset(-20); 161 | }]; 162 | 163 | self.testgetFileURLBtn = [[HLLIconButton alloc] 164 | initWithFrame:CGRectMake(20, 500, 270, 48) 165 | Icon:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"resource/navigation" 166 | ofType:@"png"]] 167 | Text:@"离线包路径"]; 168 | [self.view addSubview:self.testgetFileURLBtn]; 169 | [self.testgetFileURLBtn addTarget:self 170 | action:@selector(testgetFileURL:) 171 | forControlEvents:UIControlEventTouchUpInside]; 172 | [self.testgetFileURLBtn mas_remakeConstraints:^(MASConstraintMaker *make) { 173 | make.bottom.mas_equalTo(self.openWebviewBtn.mas_top).offset(-10); 174 | make.height.mas_equalTo(48); 175 | make.left.equalTo(self.view).offset(20); 176 | make.right.equalTo(self.view).offset(-20); 177 | }]; 178 | 179 | self.testgetOffWebBisNameBtn = [[HLLIconButton alloc] 180 | initWithFrame:CGRectMake(20, 500, 270, 48) 181 | Icon:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"resource/user" 182 | ofType:@"png"]] 183 | Text:@"离线包id"]; 184 | [self.view addSubview:self.testgetOffWebBisNameBtn]; 185 | [self.testgetOffWebBisNameBtn addTarget:self 186 | action:@selector(testgetOffWebBisName:) 187 | forControlEvents:UIControlEventTouchUpInside]; 188 | [self.testgetOffWebBisNameBtn mas_remakeConstraints:^(MASConstraintMaker *make) { 189 | make.bottom.mas_equalTo(self.testgetFileURLBtn.mas_top).offset(-10); 190 | make.height.mas_equalTo(48); 191 | make.left.equalTo(self.view).offset(20); 192 | make.right.equalTo(self.view).offset(-20); 193 | }]; 194 | } 195 | - (void)predownload:(UIButton *)button { 196 | NSLog(@"%@", button); 197 | NSString *bisName = [[HLLOfflineWebPackage getInstance] getOffWebBisName:TestUrl]; 198 | [[HLLOfflineWebPackage getInstance] 199 | checkUpdate:bisName 200 | result:^(HLLOfflineWebResultEvent result, NSString *msg) { 201 | [self displayResult:@"查询结果" 202 | Content:[NSString stringWithFormat:@"result:%ld msg:%@", (long)result, msg]]; 203 | }]; 204 | } 205 | 206 | - (void)openWebview:(UIButton *)button { 207 | HLLBisnessWebVC *vc = [[HLLBisnessWebVC alloc] initWithUrl:TestUrl]; 208 | vc.title = @"测试H5页面"; 209 | self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" 210 | style:UIBarButtonItemStylePlain 211 | target:nil 212 | action:nil]; 213 | 214 | self.navigationController.navigationBar.tintColor = [UIColor blackColor]; 215 | [self.navigationController pushViewController:vc animated:YES]; 216 | } 217 | - (void)testgetFileURL:(UIButton *)button { 218 | NSURL *filePath = [[HLLOfflineWebPackage getInstance] getFileURL:[NSURL URLWithString:TestUrl]]; 219 | [self displayResult:@"离线包路径" Content:filePath.absoluteString]; 220 | } 221 | - (void)testgetOffWebBisName:(UIButton *)button { 222 | NSString *bisName = [[HLLOfflineWebPackage getInstance] getOffWebBisName:TestUrl]; 223 | [self displayResult:@"离线包id" Content:bisName]; 224 | } 225 | 226 | - (void)displayResult:(NSString *)title Content:(NSString *)content { 227 | HLLActionSheet *actionSheet = [[HLLActionSheet alloc] initWithTitle:title Content:content]; 228 | actionSheet.modalPresentationStyle = UIModalPresentationCustom; 229 | [self presentViewController:actionSheet animated:YES completion:nil]; 230 | } 231 | - (void)didReceiveMemoryWarning { 232 | [super didReceiveMemoryWarning]; 233 | // Dispose of any resources that can be recreated. 234 | } 235 | 236 | @end 237 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/LocalServerTestController.h: -------------------------------------------------------------------------------- 1 | // 2 | // LocalServerTestController.h 3 | // HLLOfflineWebVC_Example 4 | // 5 | // Created by 货拉拉 on 2022/3/16. 6 | // Copyright © 2022 货拉拉. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | ///基于GCDWebServer搭建的本地HTTP服务,提供离线包查询和下载服务 13 | @interface LocalServerTestController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/LocalServerTestController.m: -------------------------------------------------------------------------------- 1 | #import "GCDWebServer.h" 2 | #import "GCDWebServerDataResponse.h" 3 | #import "GCDWebServerURLEncodedFormRequest.h" 4 | #import "LocalServerTestController.h" 5 | 6 | #define ServerBisName @"act3-offline-package-test" 7 | #define ServerVersion @"25609-j56gfa" 8 | @interface LocalServerTestController () { 9 | GCDWebServer *_localServer; 10 | } 11 | @end 12 | @implementation LocalServerTestController 13 | 14 | - (void)viewDidLoad { 15 | [super viewDidLoad]; 16 | // Do any additional setup after loading the view. 17 | [self startLocalServer]; 18 | } 19 | 20 | - (NSString *)dictToJsonStr:(NSDictionary *)dict { 21 | NSString *jsonString = nil; 22 | if ([NSJSONSerialization isValidJSONObject:dict]) { 23 | NSError *error; 24 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict 25 | options:NSJSONWritingPrettyPrinted 26 | error:&error]; 27 | jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 28 | if (error) { 29 | NSLog(@"Error:%@", error); 30 | } 31 | } 32 | return jsonString; 33 | } 34 | 35 | - (void)startLocalServer { 36 | _localServer = [[GCDWebServer alloc] init]; 37 | _localServer.delegate = self; 38 | //设置监听 39 | [_localServer addHandlerForMethod:@"GET" 40 | path:@"/offweb" //接口名 41 | requestClass:[GCDWebServerURLEncodedFormRequest class] 42 | processBlock:^GCDWebServerResponse *(GCDWebServerRequest *request) { 43 | GCDWebServerDataResponse *response; 44 | //获取请求中的参数(body) 45 | NSMutableDictionary *dict = [NSMutableDictionary new]; 46 | if ([request.query[@"bisName"] isEqualToString:ServerBisName] && 47 | [request.query[@"offlineZipVer"] isEqualToString:ServerVersion]) { 48 | [dict setObject:request.query[@"bisName"] forKey:@"bisName"]; 49 | [dict setObject:[NSNumber numberWithInt:0] forKey:@"result"]; 50 | 51 | } else if ([request.query[@"bisName"] isEqualToString:ServerBisName]) { 52 | [dict setObject:request.query[@"bisName"] forKey:@"bisName"]; 53 | [dict setObject:[NSNumber numberWithInt:1] forKey:@"result"]; 54 | [dict setObject:ServerVersion forKey:@"version"]; 55 | [dict setObject:[NSString stringWithFormat:@"http://localhost:5555/resource/%@.zip", 56 | ServerBisName] 57 | forKey:@"url"]; 58 | } else { 59 | [dict setObject:request.query[@"bisName"] == nil ? @"" : request.query[@"bisName"] 60 | forKey:@"bisName"]; 61 | [dict setObject:[NSNumber numberWithInt:-1] forKey:@"result"]; 62 | } 63 | response = [GCDWebServerDataResponse responseWithText:[self dictToJsonStr:dict]]; 64 | //响应头设置,跨域请求需要设置,只允许设置的域名或者ip才能跨域访问本接口) 65 | [response setValue:@"*" forAdditionalHeader:@"Access-Control-Allow-Origin"]; 66 | 67 | [response setValue:@"Content-Type" forAdditionalHeader:@"Access-Control-Allow-Headers"]; 68 | 69 | return response; 70 | }]; 71 | 72 | [_localServer addHandlerForMethod:@"GET" 73 | pathRegex:@"/resource" //接口名 74 | requestClass:[GCDWebServerURLEncodedFormRequest class] 75 | processBlock:^GCDWebServerResponse *(GCDWebServerRequest *request) { 76 | NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; 77 | NSLog(@"bundlepath %@ ", bundlePath); 78 | NSString *path = [bundlePath stringByAppendingString:request.path]; 79 | NSData *fileData = [NSData dataWithContentsOfFile:path]; 80 | GCDWebServerDataResponse *response = 81 | [GCDWebServerDataResponse responseWithData:fileData contentType:@"application/zip"]; 82 | if (fileData == nil) { 83 | return [GCDWebServerDataResponse responseWithText:@"file not exist"]; 84 | } 85 | return response; 86 | }]; 87 | 88 | //设置监听端口 89 | [_localServer startWithPort:5555 bonjourName:nil]; 90 | NSLog(@"Visit %@ in your web browser", _localServer.serverURL); 91 | } 92 | 93 | - (void)webServerDidStart:(GCDWebServer *)server { 94 | NSLog(@"本地服务启动成功"); 95 | } 96 | 97 | @end 98 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 11/02/2021. 6 | // Copyright (c) 2021 货拉拉. All rights reserved. 7 | // 8 | 9 | @import UIKit; 10 | #import "HLLAppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) 13 | { 14 | @autoreleasepool { 15 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([HLLAppDelegate class])); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/act3-offline-package-test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/act3-offline-package-test.zip -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/background.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/close.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/close1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/close1.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/logo.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/name.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/navigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/navigation.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/split.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/up.png -------------------------------------------------------------------------------- /Example/HLLOfflineWebVC/resource/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Example/HLLOfflineWebVC/resource/user.png -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | #use_frameworks! 2 | source 'https://cdn.cocoapods.org/' 3 | platform :ios, '9.0' 4 | 5 | target 'HLLOfflineWebVC_Example' do 6 | pod 'HLLOfflineWebVC', :path => '../' 7 | pod 'CRToast', '~> 0.0.7' 8 | pod 'GCDWebServer' 9 | pod 'Masonry', '1.1.0' 10 | 11 | #以下为接入的上层业务层可能会用到的功能库,比如:进行埋点数据上报等 12 | post_install do |installer| 13 | installer.pods_project.targets.each do |target| 14 | target.build_configurations.each do |config| 15 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)','WEBVCTYPE=1'] 16 | #如果需要从TransparentWebVC派生,则定义该宏 17 | # puts "===================>target build configure #{config.build_settings}" 18 | end 19 | end 20 | end 21 | 22 | target 'HLLOfflineWebVC_Tests' do 23 | inherit! :search_paths 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Example/Tests/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 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/Tests/Tests-Prefix.pch: -------------------------------------------------------------------------------- 1 | // The contents of this file are implicitly included at the beginning of every test case source file. 2 | 3 | #ifdef __OBJC__ 4 | 5 | 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /Example/Tests/Tests.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebVCTests.m 3 | // HLLOfflineWebVCTests 4 | // 5 | // Created by 货拉拉 on 11/02/2021. 6 | // Copyright (c) 2021 货拉拉. All rights reserved. 7 | // 8 | 9 | @import XCTest; 10 | 11 | @interface Tests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation Tests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown 24 | { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample 30 | { 31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 32 | } 33 | 34 | @end 35 | 36 | -------------------------------------------------------------------------------- /Example/Tests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /HLLOfflineWebVC.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint HLLOfflineWebVC.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'HLLOfflineWebVC' 11 | s.version = '1.0.0' 12 | s.summary = 'HLLOfflineWebVC 包含展示的容器webvc及离线包管理工具. subspec仅用来分组功能类' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = 'HUOLALA Offline WebVC SDK' 21 | s.homepage = 'https://xxx.com' 22 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' 23 | s.license = { :type => 'GPL', :file => 'LICENSE' } 24 | s.author = { '货拉拉' => '货拉拉' } 25 | s.source = { :git => ' ', :tag => s.version.to_s } 26 | # s.social_media_url = 'https://twitter.com/' 27 | 28 | s.ios.deployment_target = '9.0' 29 | s.pod_target_xcconfig = { 30 | 'ENABLE_BITCODE' => 'NO', 31 | 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', 32 | 'VALID_ARCHS' => 'arm64 armv7 x86_64' 33 | } 34 | 35 | s.source_files = 'HLLOfflineWebVC/Classes/*.{h,m}' 36 | # 离线包管理模块,核心模块,包含离线包查询、下载、缓存管理、数据上报功能 37 | s.subspec 'OfflineWebPackage' do |package| 38 | package.source_files = 'HLLOfflineWebVC/Classes/OfflineWebPackage/*.{h,m}' 39 | package.dependency 'HLLOfflineWebVC/OfflineWebConst' 40 | package.dependency 'HLLOfflineWebVC/OfflineWebUtils' 41 | package.dependency 'HLLOfflineWebVC/Private' 42 | end 43 | 44 | #开发者debug调试工具。方便开发和测试阶段查看和清除离线包 45 | s.subspec 'OfflineWebDevTool' do |webDevTool| 46 | webDevTool.source_files = 'HLLOfflineWebVC/Classes/OfflineWebDevTool/*.{h,m}' 47 | webDevTool.dependency 'HLLOfflineWebVC/OfflineWebPackage' #本地离线包文件删除等管理操作用到 48 | webDevTool.dependency 'HLLOfflineWebVC/OfflineWebConst' 49 | webDevTool.dependency 'CRToast' 50 | end 51 | 52 | # 一此辅助功能工具类 53 | s.subspec 'OfflineWebUtils' do |util| 54 | util.source_files = 'HLLOfflineWebVC/Classes/OfflineWebUtils/*.{h,m}' 55 | util.dependency 'HLLOfflineWebVC/OfflineWebConst' 56 | util.dependency 'SSZipArchive' #文件解压用到 57 | end 58 | 59 | # 离线包URL和bisName自动匹配 60 | s.subspec 'OfflineWebBisNameMatch' do |util| 61 | util.source_files = 'HLLOfflineWebVC/Classes/OfflineWebBisNameMatch/*.{h,m}' 62 | end 63 | 64 | # SDK内部使用的私有类 65 | s.subspec 'Private' do |_private| 66 | _private.source_files = 'HLLOfflineWebVC/Classes/Private/*.{h,m}' 67 | _private.dependency 'HLLOfflineWebVC/OfflineWebConst' 68 | _private.dependency 'HLLOfflineWebVC/OfflineWebUtils' 69 | end 70 | 71 | # 定义一些公用的常量、回调、宏等 72 | s.subspec 'OfflineWebConst' do |webConst| 73 | webConst.source_files = 'HLLOfflineWebVC/Classes/OfflineWebConst/*.{h,m}' 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/HLLOfflineWebVC/Assets/.gitkeep -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/HLLOfflineWebVC/Classes/.gitkeep -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/BaseWebViewController.h: -------------------------------------------------------------------------------- 1 | 2 | ///基于WKWebview封装的简单Webview容器 3 | #import 4 | 5 | NS_ASSUME_NONNULL_BEGIN 6 | @interface BaseWebViewController : UIViewController 7 | - (instancetype)initWithUrl:(NSString *)url; 8 | @property (nonatomic, weak, readonly) WKWebView *webView; 9 | 10 | @end 11 | 12 | NS_ASSUME_NONNULL_END 13 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/BaseWebViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | 3 | #import "BaseWebViewController.h" 4 | #import 5 | #import 6 | #import 7 | 8 | #define GetScreenWidth [[UIScreen mainScreen] bounds].size.width 9 | 10 | @interface BaseWebViewController () 11 | 12 | @property (nonatomic, weak, readwrite) WKWebView *webView; 13 | @property (nonatomic, strong, readwrite) WKUserContentController *userContentController; 14 | @property (nonatomic, copy) NSString *url; 15 | @property (nonatomic, copy) NSString *orginUrl; 16 | 17 | @end 18 | 19 | @implementation BaseWebViewController 20 | 21 | - (instancetype)initWithUrl:(NSString *)url { 22 | self = [super init]; 23 | if (self) { 24 | self.orginUrl = url; 25 | self.url = url; 26 | } 27 | return self; 28 | } 29 | 30 | - (void)load:(NSURL *)url { 31 | [self.webView loadRequest:[NSURLRequest requestWithURL:url]]; 32 | } 33 | 34 | #pragma mark - WKNavigationDelegate 35 | // 接收到服务器跳转请求之后调用 36 | - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation { 37 | NSLog(@"webView didReceiveServerRedirectForProvisionalNavigation "); 38 | } 39 | 40 | // 在收到响应后,决定是否跳转 41 | - (void)webView:(WKWebView *)webView 42 | decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse 43 | decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { 44 | long statusCode = ((NSHTTPURLResponse *)navigationResponse.response).statusCode; 45 | 46 | NSLog(@"webView decidePolicyForNavigationResponse %ld,%@", statusCode, 47 | navigationResponse.response.URL.absoluteString); 48 | //允许跳转 49 | decisionHandler(WKNavigationResponsePolicyAllow); 50 | } 51 | 52 | // 在发送请求之前,决定是否跳转 53 | - (void)webView:(WKWebView *)webView 54 | decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction 55 | decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { 56 | 57 | NSLog(@"webView decidePolicyForNavigationAction %@", navigationAction.request.URL.absoluteString); 58 | //允许跳转 59 | decisionHandler(WKNavigationActionPolicyAllow); 60 | } 61 | 62 | // 页面加载完成之后调用 63 | - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { 64 | // H5加载完成上报 65 | NSLog(@"webView didFinishNavigation "); 66 | } 67 | 68 | // 页面提交失败 69 | - (void)webView:(WKWebView *)webView 70 | didFailNavigation:(null_unspecified WKNavigation *)navigation 71 | withError:(nonnull NSError *)error { 72 | NSLog(@"webView didFailNavigation,%ld", error.code); 73 | } 74 | 75 | // 页面加载失败时调用 76 | - (void)webView:(WKWebView *)webView 77 | didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation 78 | withError:(NSError *)error { 79 | NSLog(@"webView didFailProvisionalNavigation %ld,", (long)error.code); 80 | } 81 | 82 | #pragma mark-- WKUIDelegate 83 | // 显示一个按钮。点击后调用completionHandler回调 84 | - (void)webView:(WKWebView *)webView 85 | runJavaScriptAlertPanelWithMessage:(NSString *)message 86 | initiatedByFrame:(WKFrameInfo *)frame 87 | completionHandler:(void (^)(void))completionHandler { 88 | 89 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message 90 | message:nil 91 | preferredStyle:UIAlertControllerStyleAlert]; 92 | [alertController addAction:[UIAlertAction actionWithTitle:@"确定" 93 | style:UIAlertActionStyleCancel 94 | handler:^(UIAlertAction *_Nonnull action) { 95 | completionHandler(); 96 | }]]; 97 | [self presentViewController:alertController animated:YES completion:nil]; 98 | } 99 | 100 | // 显示两个按钮,通过completionHandler回调判断用户点击的确定还是取消按钮 101 | - (void)webView:(WKWebView *)webView 102 | runJavaScriptConfirmPanelWithMessage:(NSString *)message 103 | initiatedByFrame:(WKFrameInfo *)frame 104 | completionHandler:(void (^)(BOOL))completionHandler { 105 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message 106 | message:nil 107 | preferredStyle:UIAlertControllerStyleAlert]; 108 | [alertController addAction:[UIAlertAction actionWithTitle:@"确定" 109 | style:UIAlertActionStyleDefault 110 | handler:^(UIAlertAction *_Nonnull action) { 111 | completionHandler(YES); 112 | }]]; 113 | [alertController addAction:[UIAlertAction actionWithTitle:@"取消" 114 | style:UIAlertActionStyleCancel 115 | handler:^(UIAlertAction *_Nonnull action) { 116 | completionHandler(NO); 117 | }]]; 118 | [self presentViewController:alertController animated:YES completion:nil]; 119 | } 120 | 121 | // 显示一个带有输入框和一个确定按钮的,通过completionHandler回调用户输入的内容 122 | - (void)webView:(WKWebView *)webView 123 | runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt 124 | defaultText:(NSString *)defaultText 125 | initiatedByFrame:(WKFrameInfo *)frame 126 | completionHandler:(void (^)(NSString *_Nullable))completionHandler { 127 | 128 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" 129 | message:nil 130 | preferredStyle:UIAlertControllerStyleAlert]; 131 | [alertController addTextFieldWithConfigurationHandler:^(UITextField *_Nonnull textField){ 132 | }]; 133 | [alertController addAction:[UIAlertAction actionWithTitle:@"确定" 134 | style:UIAlertActionStyleDefault 135 | handler:^(UIAlertAction *_Nonnull action) { 136 | completionHandler(alertController.textFields.lastObject.text); 137 | }]]; 138 | [self presentViewController:alertController animated:YES completion:nil]; 139 | } 140 | 141 | - (void)viewDidLoad { 142 | double beginTime = [[NSDate date] timeIntervalSince1970] * 1000; 143 | [super viewDidLoad]; 144 | // Do any additional setup after loading the view. 145 | WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init]; 146 | WKUserContentController *userContentController = [[WKUserContentController alloc] init]; 147 | webViewConfiguration.userContentController = userContentController; 148 | NSLog(@"webview inital start"); 149 | 150 | WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:webViewConfiguration]; 151 | self.webView = webView; 152 | self.webView.UIDelegate = self; 153 | self.webView.navigationDelegate = self; 154 | [self.view addSubview:webView]; 155 | [self.webView.configuration.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"]; 156 | // 可控制是否加载当前URL 157 | if (![self webview:_webView 158 | shouldStartLoadWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.url]]]) { 159 | return; 160 | } 161 | [self load:[NSURL URLWithString:self.url]]; 162 | double endTime = [[NSDate date] timeIntervalSince1970] * 1000; 163 | NSLog(@"webview inital finish"); 164 | NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults]; 165 | NSNumber *initCost = [NSNumber numberWithDouble:(endTime - beginTime)]; 166 | [userDefault setObject:initCost forKey:@"WebViewinitCost"]; 167 | } 168 | 169 | - (void)dealloc { 170 | [self.userContentController removeScriptMessageHandlerForName:@"heraldAppBridge"]; 171 | } 172 | 173 | - (BOOL)webview:(WKWebView *)webview shouldStartLoadWithRequest:(NSURLRequest *)request { 174 | return YES; 175 | } 176 | @end 177 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/HLLOfflineWebConfig.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebConfig.h 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 2021/11/2. 6 | //离线包初始化。 7 | // 1)包含实施监控、数据埋点、日志等功能实现 8 | // 2)包含离线包的功能配置,比如是否降级,需要启动时下载的离线包等配置 9 | #import "HLLOfflineWebConst.h" 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | /// MARK: 配置信息字典的相关Key定义 15 | 16 | /// 子配置项keys 17 | FOUNDATION_EXTERN NSString *const kHLLOfflineWebConfigKey_switch; 18 | FOUNDATION_EXTERN NSString *const kHLLOfflineWebConfigKey_disablelist; 19 | FOUNDATION_EXTERN NSString *const kHLLOfflineWebConfigKey_predownloadList; 20 | FOUNDATION_EXTERN NSString *const kHLLOfflineWebConfigKey_downloadSdk; 21 | 22 | #pragma mark - 23 | @interface HLLOfflineWebConfig : NSObject 24 | 25 | /// 返回包含了一个默认配置信息的字典 26 | /// @note return @{@"switch": @(0), @"predownloadlist": @[], @"downloadsdk": @(0)} 27 | /// @note 外部取值或修改数据可以用上述定义的kHLLOfflineWebConfigKey系列key值读取或修改数据,注意字典的层级结构 28 | + (NSDictionary *)defaultOffWebConfigDic; 29 | 30 | /// 离线包初始化函数 31 | /// @param offwebConfigDict 离线包配置参数, 可通过 defaultOffWebConfigDic 32 | /// 方法获取默认配置基础上修改相关默认配置项值后传入 33 | /// @param logBlock 日志block,需要外部设置日志的具体处理 34 | /// @param reportBlock 埋点block,需要外部设置埋点的具体处理 35 | /// @param monitorBlock 加载流程、信息的监控回调 36 | /// @param env 对应app运行的环境串,如 stg | pre | prd ,为nil时按prd的流程处理 37 | /// @param appVersion 当前集成的宿主app的版本串,可以直接从plist文件中读取传入即可 38 | + (void)setInitalParam:(NSDictionary *)offwebConfigDict 39 | logBlock:(HLLOfflineWebLogBlock)logBlock 40 | reportBlock:(HLLOfflineWebReportBlock)reportBlock 41 | monitorBlock:(HLLOfflineWebMonitorBlock)monitorBlock 42 | env:(NSString *_Nullable)env 43 | appversion:(NSString *)appVersion; 44 | 45 | /// 离线包预下载 46 | /// @param offwebConfigDict 离线包配置参数, 可通过 defaultOffWebConfigDic 47 | /// 方法获取默认配置基础上修改相关默认配置项值后传入 48 | /// @note 只有当配置信息中对应的switch的值为1时,调用此方法才有效,否则内部会忽略预下载处理 49 | /// @note 调用此函数前最好先调用上面的 setInital 接口,设置相关log回调 50 | + (void)predownloadOffWebPackage:(NSDictionary *)offwebConfigDict; 51 | 52 | @end 53 | 54 | NS_ASSUME_NONNULL_END 55 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/HLLOfflineWebConfig.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebConfig.m 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 2021/11/2. 6 | // 7 | 8 | #import "HLLOfflineWebConfig.h" 9 | #import "HLLOfflineWebPackage+callbacks.h" 10 | /// 子配置项keys 11 | NSString *const kHLLOfflineWebConfigKey_switch = @"switch"; 12 | NSString *const kHLLOfflineWebConfigKey_disablelist = @"disablelist"; 13 | NSString *const kHLLOfflineWebConfigKey_predownloadList = @"predownloadlist"; 14 | NSString *const kHLLOfflineWebConfigKey_downloadSdk = @"downloadsdk"; 15 | 16 | @implementation HLLOfflineWebConfig 17 | 18 | + (NSDictionary *)defaultOffWebConfigDic { 19 | return @{ 20 | kHLLOfflineWebConfigKey_switch : @(0), 21 | kHLLOfflineWebConfigKey_disablelist : @[].mutableCopy, 22 | kHLLOfflineWebConfigKey_predownloadList : @[].mutableCopy, 23 | kHLLOfflineWebConfigKey_downloadSdk : @(0) 24 | }; 25 | } 26 | 27 | + (void)setInitalParam:(NSDictionary *)offwebConfigDict 28 | logBlock:(HLLOfflineWebLogBlock)logBlock 29 | reportBlock:(HLLOfflineWebReportBlock)reportBlock 30 | monitorBlock:(HLLOfflineWebMonitorBlock)monitorBlock 31 | env:(NSString *)env 32 | appversion:(NSString *)appVersion { 33 | // switch值为1时,打开 34 | if ([offwebConfigDict isKindOfClass:[NSDictionary class]] && offwebConfigDict.count > 0) { 35 | if ([offwebConfigDict[kHLLOfflineWebConfigKey_switch] boolValue]) { 36 | [HLLOfflineWebPackage getInstance].disalbleFlag = NO; 37 | [HLLOfflineWebPackage getInstance].env = env; 38 | [[HLLOfflineWebPackage getInstance] setAppVersion:appVersion]; 39 | [HLLOfflineWebPackage getInstance].logBlock = logBlock; 40 | [HLLOfflineWebPackage getInstance].reportBlock = reportBlock; 41 | [HLLOfflineWebPackage getInstance].monitorBlock = monitorBlock; 42 | [HLLOfflineWebPackage getInstance].downloadSDKType = 43 | [offwebConfigDict[kHLLOfflineWebConfigKey_downloadSdk] intValue]; 44 | NSArray *disableList = offwebConfigDict[kHLLOfflineWebConfigKey_disablelist]; 45 | for (int i = 0; i < [disableList count]; i++) { 46 | [[HLLOfflineWebPackage getInstance] addToDisableList:disableList[i]]; 47 | } 48 | } else { 49 | [HLLOfflineWebPackage getInstance].disalbleFlag = YES; 50 | } 51 | } else { 52 | NSLog(@"[!] => offwebConfig setInital do nothing, because offwebConfigParams is: %@", offwebConfigDict); 53 | } 54 | } 55 | 56 | + (void)predownloadOffWebPackage:(NSDictionary *)offwebConfigDict { 57 | // switch值为1时,打开网络请求日志打点功能. 58 | if ([offwebConfigDict isKindOfClass:[NSDictionary class]] && offwebConfigDict.count > 0) { 59 | if ([offwebConfigDict[kHLLOfflineWebConfigKey_switch] boolValue]) { 60 | NSArray *downloadList = offwebConfigDict[kHLLOfflineWebConfigKey_predownloadList]; 61 | for (int i = 0; i < [downloadList count]; i++) { 62 | [[HLLOfflineWebPackage getInstance] checkUpdate:downloadList[i] 63 | result:^(HLLOfflineWebResultEvent result, NSString *message){ 64 | }]; 65 | } 66 | if ([HLLOfflineWebPackage getInstance].logBlock) { 67 | [HLLOfflineWebPackage getInstance].logBlock( 68 | HLLOfflineWebLogLevelInfo, @"offwebpack", 69 | [NSString stringWithFormat:@"predownload task count:%lu", (unsigned long)[downloadList count]]); 70 | } else { 71 | NSLog(@"[!] => offwebConfig missing logBlock. you should set logBlock in offwebConfig init process by " 72 | @"call function setInitalParam:... first."); 73 | } 74 | } 75 | } 76 | } 77 | @end 78 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/HLLOfflineWebPackageKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebPackageKit.h 3 | // Pods 4 | // 5 | // Created by 货拉拉 on 2022/3/11. 6 | // 7 | 8 | #ifndef HLLOfflineWebPackageKit_h 9 | #define HLLOfflineWebPackageKit_h 10 | 11 | /// 对外的统一的头文件引入,仅引入外部可访问的头文件 12 | 13 | #import 14 | #import 15 | #import 16 | 17 | #import 18 | #import 19 | 20 | #endif /* HLLOfflineWebPackageKit_h */ 21 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/HLLOfflineWebVC.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebVC.h 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 2021/11/2. 6 | // 7 | 8 | #import "BaseWebViewController.h" 9 | #import 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | ///离线包Webview容器,包含离线包管理、数据埋点,开发者工具,URL和离线包映射关系配置功能 13 | @interface HLLOfflineWebVC : BaseWebViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/HLLOfflineWebVC.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebVC.m 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 2021/11/2. 6 | // 7 | 8 | #import "HLLOfflineWebBisNameMatch.h" 9 | #import "HLLOfflineWebDataReport.h" 10 | #import "HLLOfflineWebPackage.h" 11 | #import "HLLOfflineWebVC.h" 12 | 13 | #ifdef DEBUG 14 | #import "HLLOfflineWebDevTool.h" 15 | #define DEBUG_CODE(code) code; 16 | #else 17 | #define DEBUG_CODE(code) 18 | #endif 19 | 20 | #define DefaultWebPackageConfig \ 21 | @{@"predownloadlist" : @[ @"act3-offline-package-test", @"uappweb-offline" ], @"switch" : [NSNumber numberWithInt:0]} 22 | 23 | @interface HLLOfflineWebVC () 24 | 25 | @property (nonatomic, strong) HLLOfflineWebDataReport *offwebDataReport; ///< 离线包数据上报 26 | DEBUG_CODE(@property(nonatomic, strong) HLLOfflineWebDevTool *webDevTool;) 27 | 28 | @end 29 | 30 | @implementation HLLOfflineWebVC 31 | 32 | - (void)viewDidLoad { 33 | //先初始化,以保证webvc父类的viewDidLoad处理执行了Load操作触发的相关回调执行setWebInfo 操作时,webDevTool是有效对象 34 | DEBUG_CODE(self.webDevTool = [[HLLOfflineWebDevTool alloc] init];) 35 | 36 | [super viewDidLoad]; 37 | // Do any additional setup after loading the view. 38 | 39 | DEBUG_CODE([self.webDevTool attachToParentVc:self];) 40 | } 41 | 42 | - (HLLOfflineWebDataReport *)offwebDataReport { 43 | if (!_offwebDataReport) { 44 | _offwebDataReport = [[HLLOfflineWebDataReport alloc] init]; 45 | } 46 | return _offwebDataReport; 47 | } 48 | 49 | - (BOOL)webview:(WKWebView *)webview shouldStartLoadWithRequest:(NSURLRequest *)request { 50 | NSString *newUrlStr = [HLLOfflineWebBisNameMatch filterWebURLString:request.URL.absoluteString 51 | baseConfig:DefaultWebPackageConfig]; 52 | if ([self doOffwebLogic:newUrlStr]) { 53 | return NO; 54 | } 55 | return YES; 56 | } 57 | 58 | - (BOOL)doOffwebLogic:(NSString *)url { 59 | NSString *bisName = [[HLLOfflineWebPackage getInstance] getOffWebBisName:url]; 60 | self.offwebDataReport.bisName = bisName; 61 | [self.offwebDataReport notifyWebEvent:HLLOfflineWebDataReportEventWebviewStartLoad 62 | url:[NSURL URLWithString:url] 63 | code:0 64 | errMsg:@""]; 65 | if (bisName != nil && [bisName length] > 0) { 66 | NSURL *fileUrl = [[HLLOfflineWebPackage getInstance] getFileURL:[NSURL URLWithString:url]]; 67 | if (fileUrl == nil) { 68 | [self _hllofflineWebVc_load:[NSURL URLWithString:url]]; 69 | } else { 70 | [self _hllofflineWebVc_load:fileUrl]; 71 | } 72 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 73 | [[HLLOfflineWebPackage getInstance] 74 | checkUpdate:bisName 75 | result:^(HLLOfflineWebResultEvent result, NSString *message) { 76 | if (result == HLLOfflineWebRefreshPackageNow) { 77 | [self _hllofflineWebVc_load:[NSURL URLWithString:@"about:blank"]]; 78 | NSURL *fileUrl = [[HLLOfflineWebPackage getInstance] getFileURL:[NSURL URLWithString:url]]; 79 | [self _hllofflineWebVc_load:fileUrl]; 80 | NSLog(@"强制刷新"); 81 | } else if (result == HLLOfflineWebRefreshOnlineWebNow) { 82 | if (self.webView.URL.isFileURL) { 83 | [self _hllofflineWebVc_load:[NSURL URLWithString:url]]; 84 | } 85 | NSLog(@"配置走线上H5"); 86 | } else if (result == HLLOfflineWebRefreshPackageLater) { 87 | NSLog(@"离线包解压成功,下次生效"); 88 | } 89 | }]; 90 | }); 91 | 92 | return YES; 93 | } 94 | return NO; 95 | } 96 | 97 | - (void)_hllofflineWebVc_load:(NSURL *)url { 98 | if (url.isFileURL) { 99 | NSString *documentPath = 100 | NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 101 | NSString *accessPath = [documentPath stringByAppendingString:@"/offlineWeb"]; 102 | [self.webView loadFileURL:url allowingReadAccessToURL:[NSURL fileURLWithPath:accessPath]]; 103 | } else { 104 | [self.webView loadRequest:[NSURLRequest requestWithURL:url]]; 105 | } 106 | } 107 | 108 | - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { 109 | [super webView:webView didFinishNavigation:navigation]; 110 | //离线包加载完成上报 111 | [self.offwebDataReport notifyWebEvent:HLLOfflineWebDataReportEventWebviewLoadSuccess 112 | url:webView.URL 113 | code:0 114 | errMsg:@"finish"]; 115 | } 116 | 117 | //页面加载失败 118 | - (void)webView:(WKWebView *)webView 119 | didFailProvisionalNavigation:(WKNavigation *)navigation 120 | withError:(NSError *)error { 121 | [super webView:webView didFailProvisionalNavigation:navigation withError:error]; 122 | //离线包加载失败上报 123 | [self.offwebDataReport notifyWebEvent:HLLOfflineWebDataReportEventWebviewLoadFail 124 | url:webView.URL 125 | code:error.code 126 | errMsg:error.description]; 127 | } 128 | 129 | // 页面提交失败 130 | - (void)webView:(WKWebView *)webView 131 | didFailNavigation:(null_unspecified WKNavigation *)navigation 132 | withError:(nonnull NSError *)error { 133 | [super webView:webView didFailNavigation:navigation withError:error]; 134 | //离线包加载失败上报 135 | [self.offwebDataReport notifyWebEvent:HLLOfflineWebDataReportEventWebviewLoadFail 136 | url:webView.URL 137 | code:error.code 138 | errMsg:error.description]; 139 | } 140 | 141 | // 准备加载页面 142 | - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { 143 | 144 | [self.offwebDataReport notifyWebEvent:HLLOfflineWebDataReportEventWebviewWillRequest 145 | url:webView.URL 146 | code:0 147 | errMsg:@""]; 148 | 149 | DEBUG_CODE([self.webDevTool setWebInfo:webView.URL bisName:self.offwebDataReport.bisName];) 150 | } 151 | 152 | // 在收到响应后,决定是否跳转 153 | - (void)webView:(WKWebView *)webView 154 | decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse 155 | decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { 156 | [super webView:webView decidePolicyForNavigationResponse:navigationResponse decisionHandler:decisionHandler]; 157 | long statusCode = ((NSHTTPURLResponse *)navigationResponse.response).statusCode; 158 | [self.offwebDataReport notifyWebEvent:HLLOfflineWebDataReportEventWebviewReceiveResponse 159 | url:webView.URL 160 | code:statusCode 161 | errMsg:@""]; 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebBisNameMatch/HLLOfflineWebBisNameMatch.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebBisNameMatch.h 3 | // 4 | // Created by 货拉拉 on 2022/1/15. 5 | // 6 | 7 | #import 8 | 9 | /// URL和离线包通用关系映射 10 | /// 未了解决开启离线包需要修改URL添加离线包参数繁琐的问题,通过远程配置,给命中规则的H5页面自动添加离线包参数 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface HLLOfflineWebBisNameMatch : NSObject 15 | 16 | /// 处理H5链接,给命中匹配规则的URL添加离线包参数 17 | /// @param string 原始请求地址 18 | /// @param baseConfig 远程配置上的json值 19 | /// @return 处理后的地址,未匹配上则返回原地址 20 | + (NSString *)filterWebURLString:(NSString *)string baseConfig:(NSDictionary *)baseConfig; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebBisNameMatch/HLLOfflineWebBisNameMatch.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebBisNameMatch.m 3 | // 123456 4 | // 5 | // Created by 货拉拉 on 2022/1/15. 6 | // 7 | 8 | #import "HLLOfflineWebBisNameMatch.h" 9 | 10 | @interface HLLWebCacheItem : NSObject 11 | 12 | @property (nonatomic, copy) NSArray *hosts; // 缓存域名 13 | @property (nonatomic, copy) NSArray *paths; // 缓存路径 14 | @property (nonatomic, copy) NSArray *fragmentprefixs; // 缓存页面路由名 15 | @property (nonatomic, copy) NSString *offweb; // 缓存类型 16 | 17 | @end 18 | 19 | @implementation HLLWebCacheItem 20 | 21 | + (instancetype)createWebCacheItemWithConf:(NSDictionary *)config { 22 | HLLWebCacheItem *item = [[HLLWebCacheItem alloc] init]; 23 | item.hosts = [self arrayResult:config[@"host"]]; 24 | item.paths = [self arrayResult:config[@"path"]]; 25 | item.fragmentprefixs = [self arrayResult:config[@"fragmentprefix"]]; 26 | item.offweb = [NSString stringWithFormat:@"%@", config[@"offweb"]]; 27 | return item; 28 | } 29 | 30 | + (NSArray *)arrayResult:(id)result { 31 | if (result && [result isKindOfClass:[NSArray class]]) { 32 | return result; 33 | } 34 | return @[]; 35 | } 36 | 37 | @end 38 | 39 | @implementation HLLOfflineWebBisNameMatch 40 | 41 | + (NSString *)filterWebURLString:(NSString *)string baseConfig:(NSDictionary *)baseConfig { 42 | if (baseConfig && [baseConfig isKindOfClass:[NSDictionary class]]) { 43 | NSString *offlineUrl = @""; 44 | if (string && [string isKindOfClass:[NSString class]] && string.length > 0) { 45 | NSURL *url = [NSURL URLWithString:string]; 46 | if (url) { 47 | if ([string containsString:@"/#/"]) { 48 | // 1.采用SPA单页面路由 49 | offlineUrl = [self parserSPAUrl:url baseConfig:baseConfig]; 50 | // 2.兼容线上已有的地址 51 | } else if ([string containsString:@"#/"] && ![string containsString:@"/#/"]) { 52 | offlineUrl = [self parserSPAHandledUrl:url baseConfig:baseConfig]; 53 | } else { 54 | // 3.一般H5页面地址 55 | offlineUrl = [self parserNormalUrl:url baseConfig:baseConfig]; 56 | } 57 | } 58 | } 59 | return offlineUrl; 60 | } 61 | return string; 62 | } 63 | 64 | // mock数据测试 65 | + (NSDictionary *)requestMockConfig { 66 | return @{ 67 | @"rules" : @[ 68 | @{ 69 | @"host" : @[ @"test1.huolala.cn", @"www.baidu.com" ], 70 | @"path" : @[ @"/uapp", @"/abc/123", @"/uapp/cd-index.html", @"/uapp/*/cd-index-abc" ], 71 | @"fragmentprefix" : @[ @"/cd-index", @"12" ], 72 | @"offweb" : @"uappweb-offline_update" 73 | }, 74 | @{ 75 | @"host" : @[ @"test2.huolala.cn", @"www.baidu1.com" ], 76 | @"path" : @[ @"/uapp", @"/abc/123" ], 77 | @"fragment" : @[ @"/cd-index" ], 78 | @"offweb" : @"uappweb-offline" 79 | } 80 | ] 81 | }; 82 | } 83 | 84 | + (NSArray *)requestCacheConf:(NSDictionary *)baseConfig { 85 | NSDictionary *conf = baseConfig; 86 | NSMutableArray *tempArr = [NSMutableArray array]; 87 | NSArray *rules = conf[@"rules"]; 88 | if (rules && [rules isKindOfClass:[NSArray class]] && rules.count > 0) { 89 | for (NSInteger i = 0; i < rules.count; i++) { 90 | NSDictionary *dict = rules[i]; 91 | if (dict && [dict isKindOfClass:[NSDictionary class]]) { 92 | // host path offweb 不能为空 93 | NSArray *host = dict[@"host"]; 94 | NSArray *path = dict[@"path"]; 95 | NSString *offweb = dict[@"offweb"]; 96 | if ([self arrayValid:host] && [self arrayValid:path] && [self stringValid:offweb]) { 97 | HLLWebCacheItem *item = [HLLWebCacheItem createWebCacheItemWithConf:dict]; 98 | [tempArr addObject:item]; 99 | } 100 | } 101 | } 102 | } 103 | return tempArr; 104 | } 105 | 106 | + (BOOL)arrayValid:(NSArray *)array { 107 | if (array && [array isKindOfClass:[NSArray class]] && array.count > 0) { 108 | return YES; 109 | } 110 | return NO; 111 | } 112 | 113 | + (BOOL)stringValid:(NSString *)string { 114 | if (string && [string isKindOfClass:[NSString class]] && string.length > 0) { 115 | return YES; 116 | } 117 | return NO; 118 | } 119 | 120 | + (NSString *)parserSPAUrl:(NSURL *)URL baseConfig:(NSDictionary *)baseConfig { 121 | NSString *offweb = [self checkNeedCacheURL:URL baseConfig:baseConfig]; 122 | if (offweb && [offweb isKindOfClass:[NSString class]] && offweb.length > 0) { 123 | NSString *query = URL.query; 124 | if (query && [query containsString:@"="]) { 125 | query = [self handleHasQuerySPAUrl:URL offweb:offweb]; 126 | return [NSString stringWithFormat:@"%@://%@%@?%@#%@", URL.scheme, URL.host, URL.path, query, URL.fragment]; 127 | } 128 | return 129 | [NSString stringWithFormat:@"%@://%@%@?offweb=%@#%@", URL.scheme, URL.host, URL.path, offweb, URL.fragment]; 130 | } else { 131 | return URL.absoluteString; 132 | } 133 | } 134 | 135 | + (NSString *)handleHasQuerySPAUrl:(NSURL *)URL offweb:(NSString *)offweb { 136 | NSString *query = URL.query; 137 | if (query && [query containsString:@"offweb="]) { 138 | // H5链接自带offweb参数,覆盖掉 139 | NSArray *arr = [query componentsSeparatedByString:@"&"]; 140 | NSMutableString *muStr = [NSMutableString string]; 141 | for (int i = 0; i < arr.count; i++) { 142 | NSString *val = arr[i]; 143 | if ([val containsString:@"offweb="]) { 144 | [muStr appendString:[NSString stringWithFormat:@"offweb=%@", offweb]]; 145 | } else { 146 | [muStr appendString:val]; 147 | } 148 | if (i != arr.count - 1) { 149 | [muStr appendString:@"&"]; 150 | } 151 | } 152 | return muStr; 153 | } 154 | return [NSString stringWithFormat:@"%@&offweb=%@", query, offweb]; 155 | } 156 | 157 | + (NSString *)checkNeedCacheURL:(NSURL *)URL baseConfig:(NSDictionary *)baseConfig { 158 | NSString *final_offweb = @""; 159 | NSArray *items = [self requestCacheConf:baseConfig]; 160 | for (NSInteger i = 0; i < items.count; i++) { 161 | HLLWebCacheItem *item = items[i]; 162 | NSArray *hosts = item.hosts; 163 | NSArray *paths = item.paths; 164 | NSArray *fragmentprefixs = item.fragmentprefixs; 165 | NSString *offweb = item.offweb; 166 | if ([self checkHost:URL.host config:hosts] && [self checkPath:URL.path config:paths] && 167 | [self checkFragmentprefix:URL.fragment config:fragmentprefixs]) { 168 | final_offweb = offweb; 169 | } 170 | } 171 | return final_offweb; 172 | } 173 | 174 | + (BOOL)checkHost:(NSString *)host config:(NSArray *)hosts { 175 | BOOL result = NO; 176 | for (NSInteger j = 0; j < hosts.count; j++) { 177 | NSString *indexHost = [NSString stringWithFormat:@"%@", hosts[j]]; 178 | if ([host isEqualToString:indexHost]) { 179 | result = YES; 180 | break; 181 | } 182 | } 183 | return result; 184 | } 185 | 186 | + (BOOL)checkPath:(NSString *)path config:(NSArray *)paths { 187 | BOOL result = NO; 188 | for (NSInteger j = 0; j < paths.count; j++) { 189 | NSString *indexPath = [NSString stringWithFormat:@"%@", paths[j]]; 190 | NSArray *pathSplitArray = [path componentsSeparatedByString:@"/"]; 191 | NSArray *indexPathSplitArray = [indexPath componentsSeparatedByString:@"/"]; 192 | BOOL compareRes = [self compareSplitPath:pathSplitArray indexPathSplit:indexPathSplitArray]; 193 | if (compareRes) { 194 | result = YES; 195 | break; 196 | } 197 | } 198 | return result; 199 | } 200 | 201 | + (BOOL)compareSplitPath:(NSArray *)pathSplitArray indexPathSplit:(NSArray *)indexPathSplitArray { 202 | BOOL result = YES; 203 | if (pathSplitArray.count != indexPathSplitArray.count) { 204 | result = NO; 205 | } else { 206 | for (NSInteger i = 0; i < pathSplitArray.count; i++) { 207 | NSString *path = [NSString stringWithFormat:@"%@", pathSplitArray[i]]; 208 | NSString *indexPath = [NSString stringWithFormat:@"%@", indexPathSplitArray[i]]; 209 | if ([indexPath isEqualToString:@"*"]) { 210 | continue; 211 | } 212 | if (![path isEqualToString:indexPath]) { 213 | result = NO; 214 | break; 215 | } 216 | } 217 | } 218 | return result; 219 | } 220 | 221 | + (BOOL)checkFragmentprefix:(NSString *)fragmentprefix config:(NSArray *)fragmentprefixs { 222 | fragmentprefix = [NSString stringWithFormat:@"%@", fragmentprefix]; 223 | if (fragmentprefixs.count == 0) { 224 | return NO; 225 | } 226 | fragmentprefix = [self parserFragmentprefix:fragmentprefix]; 227 | BOOL result = NO; 228 | if (fragmentprefix && [fragmentprefix isKindOfClass:[NSString class]]) { 229 | for (NSInteger j = 0; j < fragmentprefixs.count; j++) { 230 | NSString *indexFragmentprefix = [NSString stringWithFormat:@"%@", fragmentprefixs[j]]; 231 | if ([fragmentprefix isEqualToString:indexFragmentprefix]) { 232 | result = YES; 233 | break; 234 | } 235 | } 236 | } 237 | return result; 238 | } 239 | 240 | + (NSString *)parserFragmentprefix:(NSString *)fragmentprefix { 241 | NSInteger index = 0; 242 | BOOL charFlag = NO; 243 | for (NSInteger i = 0; i < [fragmentprefix length]; i++) { 244 | NSString *cha = [fragmentprefix substringWithRange:NSMakeRange(i, 1)]; 245 | if ([cha isEqualToString:@"?"]) { 246 | charFlag = YES; 247 | index = i; 248 | break; 249 | } 250 | } 251 | if (charFlag) { 252 | return [fragmentprefix substringToIndex:index]; 253 | } 254 | return fragmentprefix; 255 | } 256 | 257 | + (NSString *)parserSPAHandledUrl:(NSURL *)URL baseConfig:(NSDictionary *)baseConfig { 258 | // 3.带有#/,则覆盖线上offweb字段 259 | return [self parserSPAUrl:URL baseConfig:baseConfig]; 260 | } 261 | 262 | + (NSString *)parserNormalUrl:(NSURL *)URL baseConfig:(NSDictionary *)baseConfig { 263 | // 页面不带有#,如果H5参数有offweb字段则匹配上后直接覆盖掉 264 | NSString *offweb = [self checkNeedCacheNoFragmentURL:URL baseConfig:baseConfig]; 265 | if (offweb && [offweb isKindOfClass:[NSString class]] && offweb.length > 0) { 266 | return [self coverURLQueryWithOffweb:offweb URL:URL]; 267 | } else { 268 | return URL.absoluteString; 269 | } 270 | } 271 | 272 | + (NSString *)coverURLQueryWithOffweb:(NSString *)offweb URL:(NSURL *)URL { 273 | NSString *query = URL.query; 274 | if (query && [query containsString:@"offweb="]) { 275 | // H5链接自带offweb参数,覆盖掉 276 | NSArray *arr = [query componentsSeparatedByString:@"&"]; 277 | NSMutableString *muStr = [NSMutableString string]; 278 | for (int i = 0; i < arr.count; i++) { 279 | NSString *val = arr[i]; 280 | if ([val containsString:@"offweb="]) { 281 | [muStr appendString:[NSString stringWithFormat:@"offweb=%@", offweb]]; 282 | } else { 283 | [muStr appendString:val]; 284 | } 285 | if (i != arr.count - 1) { 286 | [muStr appendString:@"&"]; 287 | } 288 | } 289 | return [NSString stringWithFormat:@"%@://%@%@?%@", URL.scheme, URL.host, URL.path, muStr]; 290 | } else { 291 | // H5链接不带offweb参数,直接拼接 292 | if ([URL.absoluteString containsString:@"?"]) { 293 | if (URL.query && [URL.query containsString:@"="]) { 294 | return [NSString stringWithFormat:@"%@&offweb=%@", URL.absoluteString, offweb]; 295 | } else { 296 | return [NSString stringWithFormat:@"%@offweb=%@", URL.absoluteString, offweb]; 297 | } 298 | } else { 299 | return [NSString stringWithFormat:@"%@?offweb=%@", URL.absoluteString, offweb]; 300 | } 301 | } 302 | } 303 | 304 | + (NSString *)checkNeedCacheNoFragmentURL:(NSURL *)URL baseConfig:(NSDictionary *)baseConfig { 305 | NSString *final_offweb = @""; 306 | NSArray *items = [self requestCacheConf:baseConfig]; 307 | for (NSInteger i = 0; i < items.count; i++) { 308 | HLLWebCacheItem *item = items[i]; 309 | NSArray *hosts = item.hosts; 310 | NSArray *paths = item.paths; 311 | NSString *offweb = item.offweb; 312 | if ([self checkHost:URL.host config:hosts] && [self checkPath:URL.path config:paths] && 313 | [self checkFragmentprefix:@"" config:item.fragmentprefixs]) { 314 | final_offweb = offweb; 315 | } 316 | } 317 | return final_offweb; 318 | } 319 | 320 | @end 321 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebConst/HLLOfflineWebConst.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebConst.h 3 | // Pods 4 | // 5 | // Created by 货拉拉 on 2022/3/11. 6 | // 7 | 8 | #ifndef HLLOfflineWebConst_h 9 | #define HLLOfflineWebConst_h 10 | 11 | #pragma mark - 常量定义 12 | 13 | /// 对应的一些操作事件或状态定义 14 | typedef NS_ENUM(NSInteger, HLLOfflineWebResultEvent) { 15 | HLLOfflineWebDisable = -17, ///< 暂时禁用了离线包 16 | HLLOfflineWebParseError = -16, ///< 相关数据解析失败 17 | HLLOfflineWebQueryError = -14, ///< 检查更新出错 18 | HLLOfflineWebDownloadError = -13, ///< 下载出错 - 19 | HLLOfflineWebUnzipError = -12, ///< 解压出错 20 | HLLOfflineWebDownloadCancel = -1, ///< 下载取消 - 未用到 21 | HLLOfflineWebDownloading = -4, ///< 处于下载中 22 | HLLOfflineWebUnzipSuccess = 0, ///< 解压成功 23 | 24 | HLLOfflineWebRefreshPackageLater = 1, ///< 离线包已下载,下次生效 25 | HLLOfflineWebRefreshPackageNow = 2, ///< 离线包已下载,马上生效 26 | HLLOfflineWebRefreshOnlineWebNow = 3, ///< 刷新线上页面,即若当前是离线包加载,收到此事件时应该马上刷新加载线上页面 27 | 28 | HLLOfflineWebNoUpdate = 10, ///< 检查离线包更新,返回线上无更新 29 | HLLOfflineWebDownloadSuccess = 11, ///< 离线包下载成功 30 | }; 31 | 32 | /// 对应logBlock中的传递的level值,上层业务需要关注此回调的具体值 33 | typedef NS_ENUM(NSInteger, HLLOfflineWebLogLevel) { 34 | HLLOfflineWebLogLevelError = 0, ///< 错误日志级别 35 | HLLOfflineWebLogLevelWarning = 1, ///<警告日志级别 36 | HLLOfflineWebLogLevelInfo = 2, ///< 信息日志级别 37 | HLLOfflineWebLogLevelDebug = 3, ///< 调试日志级别 38 | }; 39 | 40 | /// 对应monitorBlock中传递的type值,上层业务需要关注此回调的具体值 41 | typedef NS_ENUM(NSInteger, HLLOfflineWebMonitorType) { 42 | HLLOfflineWebMonitorTypeCounter = 0, ///<计数型监控 43 | HLLOfflineWebMonitorTypeSummary = 1, ///<求和型监控 44 | }; 45 | 46 | /// 下载SDK类型 47 | typedef NS_ENUM(NSInteger, HLLOfflineWebDownloadType) { 48 | HLLOfflineWebDownloadTypeSystemAPI = 0, ///<系统API下载方式。 49 | }; 50 | 51 | #pragma mark - 回调定义: 外部和内部都会用到 52 | 53 | /// 相关操作的回调block 54 | typedef void (^HLLOfflineWebResultBlock)(HLLOfflineWebResultEvent event, NSString *desc); 55 | 56 | /// 定义外部自定义的日志回调block | 57 | /// 参数与当前argus日志库的调用参数保持一致,上层注意区分level值与日志库中的定义是否一致,若不一致则需要自行映射转换 58 | typedef void (^HLLOfflineWebLogBlock)(HLLOfflineWebLogLevel level, NSString *keyword, NSString *message); 59 | 60 | /// 定义外部自定义的埋点数据上报回调block | 参数与当前神策SDK上报时的调用参数保持一致 61 | typedef void (^HLLOfflineWebReportBlock)(NSString *event, NSDictionary *dict); 62 | 63 | /// 定义外部自定义的监控上报回调block | 参数与argus日志库中的实时上报的调用参数保持一致,type用来区分指标类型 64 | typedef void (^HLLOfflineWebMonitorBlock)(HLLOfflineWebMonitorType type, NSString *key, CGFloat value, 65 | NSDictionary *lables); 66 | 67 | #endif /* HLLOfflineWebConst_h */ 68 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebDevTool/HLLOfflineWebDevTool.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebVC.m 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 2021/11/2. 6 | // 7 | 8 | #import 9 | 10 | /// Debug模式下,离线包调试工具 11 | @interface HLLOfflineWebDevTool : NSObject 12 | 13 | /// 附加到指定的vc对应的视图上展示,通常parentVc为对应的webvc对象 14 | /// @note 通常此方法在parentVc的viewDidLoad中调用,可以保证不影响parentVc的相关系统级view出现消失的事件回调时机 15 | - (void)attachToParentVc:(UIViewController *)parentVc; 16 | 17 | /// 设置开发工具对应的当前页的相关数据 18 | /// @param nsurl 当前页面Url 19 | /// @param bisName 当前页面对应的离线包的业务名 20 | /// @note 点击开发调试工具按钮时会用到这里设置的相关数据 21 | - (void)setWebInfo:(NSURL *)nsurl bisName:(NSString *)bisName; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebDevTool/HLLOfflineWebDevTool.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebVC.m 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 2021/11/2. 6 | // 7 | 8 | #import "HLLOfflineWebDevTool.h" 9 | #import "HLLOfflineWebFileMgr.h" 10 | #import 11 | @interface HLLOfflineWebDevTool () 12 | 13 | @property (nonatomic, weak) UIViewController *parentVC; 14 | @property (nonatomic, strong) UIPanGestureRecognizer *panGesRecognizer; 15 | @property (nonatomic, strong) UIButton *webDevBtn; 16 | @property (nonatomic, strong) NSURL *nsurl; 17 | @property (nonatomic, copy) NSString *bisName; 18 | @end 19 | 20 | @implementation HLLOfflineWebDevTool 21 | 22 | - (void)attachToParentVc:(UIViewController *)parentVc { 23 | self.parentVC = parentVc; 24 | [parentVc.view addSubview:self.webDevBtn]; 25 | } 26 | 27 | - (UIButton *)webDevBtn { 28 | if (!_webDevBtn) { 29 | _webDevBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 30 | _webDevBtn.titleLabel.numberOfLines = 0; 31 | _webDevBtn.titleLabel.font = [UIFont systemFontOfSize:12]; 32 | [_webDevBtn setTitle:@" 开发者\n 工具" forState:UIControlStateNormal]; 33 | [_webDevBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 34 | _webDevBtn.backgroundColor = [UIColor colorWithRed:241.0/255 green:102.0/255 blue:34.0/255 alpha:1]; 35 | _webDevBtn.layer.cornerRadius = 25; 36 | [_webDevBtn setFrame:CGRectMake(10, 100, 50, 50)]; 37 | //拖拽手势 38 | self.panGesRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self 39 | action:@selector(onPanGesRecognizer:)]; 40 | [_webDevBtn addGestureRecognizer:self.panGesRecognizer]; 41 | [_webDevBtn addTarget:self action:@selector(testBtnClick) forControlEvents:UIControlEventTouchUpInside]; 42 | } 43 | return _webDevBtn; 44 | } 45 | 46 | - (void)testBtnClick { 47 | NSString *displayLabel = @"加载未完成,请稍后"; 48 | NSString *offwebLabel = @"未开启离线包功能"; 49 | if (self.nsurl != nil) { 50 | if (self.nsurl.isFileURL) { 51 | displayLabel = @"离线包模式"; 52 | } else { 53 | displayLabel = @"线上网页模式"; 54 | } 55 | if (self.bisName) { 56 | offwebLabel = 57 | [NSString stringWithFormat:@"离线包版本:%@", [HLLOfflineWebFileMgr getDiskCurVersion:self.bisName]]; 58 | } 59 | } 60 | 61 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:displayLabel 62 | message:offwebLabel 63 | preferredStyle:UIAlertControllerStyleActionSheet]; 64 | [alertController addAction:[UIAlertAction actionWithTitle:@"清除离线包" 65 | style:UIAlertActionStyleDefault 66 | handler:^(UIAlertAction *_Nonnull action) { 67 | BOOL ret = [HLLOfflineWebFileMgr deleteDiskCache]; 68 | if (ret) { 69 | [CRToastManager showNotificationWithMessage:@"清除离线包成功" 70 | completionBlock:^{ 71 | NSLog(@"Completed"); 72 | }]; 73 | 74 | } else { 75 | [CRToastManager showNotificationWithMessage:@"清除离线包失败" 76 | completionBlock:^{ 77 | NSLog(@"Completed"); 78 | }]; 79 | } 80 | }]]; 81 | 82 | [alertController addAction:[UIAlertAction actionWithTitle:@"取消" 83 | style:UIAlertActionStyleCancel 84 | handler:^(UIAlertAction *_Nonnull action) { 85 | NSLog(@"点击取消"); 86 | }]]; 87 | // 由于它是一个控制器 直接modal出来就好了 88 | [self.parentVC presentViewController:alertController animated:YES completion:nil]; 89 | } 90 | 91 | - (void)onPanGesRecognizer:(UIPanGestureRecognizer *)ges { 92 | if (ges.state == UIGestureRecognizerStateChanged || ges.state == UIGestureRecognizerStateEnded) { 93 | // translationInView:获取到的是手指移动后,在相对坐标中的偏移量 94 | CGPoint offset = [ges translationInView:self.parentVC.view]; 95 | CGPoint center = CGPointMake(self.webDevBtn.center.x + offset.x, self.webDevBtn.center.y + offset.y); 96 | [self.webDevBtn setCenter:center]; 97 | [ges setTranslation:CGPointMake(0, 0) inView:self.parentVC.view]; 98 | } 99 | } 100 | 101 | - (void)setWebInfo:(NSURL *)nsurl bisName:(NSString *)bisName { 102 | self.nsurl = nsurl; 103 | self.bisName = bisName; 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebPackage/HLLOfflineWebDataReport.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebviewDataReport.h 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/8/11. 6 | //离线包埋点功能 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | /// 监控上报事件枚举定义 13 | typedef NS_ENUM(NSInteger, HLLOfflineWebDataReportEvent) { 14 | HLLOfflineWebDataReportEventWebviewStartLoad = 0, /// WKWebView容器初始化URL, 15 | HLLOfflineWebDataReportEventWebviewLoadSuccess = 1, /// WKWebView didFinishNavigation事件触发,表示网络请求成功 16 | HLLOfflineWebDataReportEventWebviewLoadFail = 2, /// WKWebView didFailProvisionalNavigation和didFailNavigation触发,表示加载失败 17 | HLLOfflineWebDataReportEventWebviewWillRequest = 3, /// 每次发起请求前触发,首次打开或者容器内跳转其他URL都会触发 18 | HLLOfflineWebDataReportEventWebviewReceiveResponse = 4, /// 收到http返回码时触发 19 | }; 20 | 21 | /// 用于webview加载页面流程的相关监控数据上报,主要是为了监控离线包的相关性能数据 22 | /// @note 但非离线包页面的加载对应的数据也会被上报 (这里是故意不进行过滤,需要统计线上加载和离线包加载的数据,已确认过) 23 | @interface HLLOfflineWebDataReport : NSObject 24 | 25 | /// 离线包业务的名称,在对应页面首次将要加载时进行赋值 26 | /// @note 通常在 - (BOOL)webview:(WKWebView *)webview shouldStartLoadWithRequest:(NSURLRequest *)request 27 | /// 回调中进行bisName的解析赋值 28 | @property (nonatomic, copy) NSString *bisName; 29 | 30 | /// 内部或外部使用的监控数据上报接口 31 | /// @param event 上报事件类型 32 | /// @param url 当前对应加载的url对象 33 | /// @param errCode 错误码, 成功时传0即可 34 | /// @param errMsg 错误描述信息,成功时传nil即可 35 | - (void)notifyWebEvent:(HLLOfflineWebDataReportEvent)event 36 | url:(NSURL *)url 37 | code:(long)errCode 38 | errMsg:(NSString *_Nullable)errMsg; 39 | 40 | @end 41 | 42 | NS_ASSUME_NONNULL_END 43 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebPackage/HLLOfflineWebDataReport.m: -------------------------------------------------------------------------------- 1 | // 2 | // WebviewDataReport.m 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/8/11. 6 | // 7 | 8 | #import "HLLOfflineWebDataReport.h" 9 | #import "HLLOfflineWebPackage+callbacks.h" 10 | 11 | @interface HLLOfflineWebDataReport () 12 | 13 | @property (nonatomic, strong) NSURL *originURL; 14 | @property (nonatomic, assign) CFAbsoluteTime startQueryTime; ///< webview启动时间戳 15 | @property (nonatomic, assign) CFAbsoluteTime willQueryTime; ///< webview发起网络请求时间戳。因为wekit初始化有时间,所以和startQuery有相差 16 | @property (nonatomic, assign) NSInteger httpResponseCode; 17 | 18 | @end 19 | 20 | @implementation HLLOfflineWebDataReport 21 | 22 | - (instancetype)init { 23 | self = [super init]; 24 | if (self) { 25 | self.startQueryTime = 0; 26 | self.willQueryTime = 0; 27 | } 28 | return self; 29 | } 30 | 31 | - (void)notifyWebEvent:(HLLOfflineWebDataReportEvent)event 32 | url:(NSURL *)url 33 | code:(long)errCode 34 | errMsg:(NSString *)errMsg { 35 | 36 | //全局开关暂停时,不做任何上报 37 | if ([HLLOfflineWebPackage getInstance].disalbleFlag) { 38 | return; 39 | } 40 | 41 | if (event == HLLOfflineWebDataReportEventWebviewStartLoad) { 42 | self.startQueryTime = CFAbsoluteTimeGetCurrent(); 43 | } else if (event == HLLOfflineWebDataReportEventWebviewWillRequest) { 44 | self.willQueryTime = CFAbsoluteTimeGetCurrent(); 45 | self.originURL = url; 46 | } else if (event == HLLOfflineWebDataReportEventWebviewReceiveResponse) { 47 | //暂存http返回码 48 | self.httpResponseCode = errCode; 49 | } else if (event == HLLOfflineWebDataReportEventWebviewLoadSuccess || 50 | event == HLLOfflineWebDataReportEventWebviewLoadFail) { 51 | if (self.originURL == nil) { 52 | return; 53 | } 54 | 55 | NSMutableDictionary *dataDict = [NSMutableDictionary dictionary]; 56 | [dataDict setObject:self.originURL.absoluteString forKey:@"url"]; 57 | NSString *simpleUrl = 58 | [NSString stringWithFormat:@"%@://%@%@", self.originURL.scheme, self.originURL.host, self.originURL.path]; 59 | if ([self.originURL isFileURL]) { 60 | simpleUrl = @"file:///cur/index.html"; 61 | } 62 | [dataDict setObject:simpleUrl forKey:@"simpleUrl"]; 63 | 64 | // CFAbsoluteTimeGetCurrent()返回的时间戳单位是: 微秒 65 | int costTime = 0; 66 | if (self.startQueryTime != 0) { 67 | // webview容器首次打开和加载H5开始 68 | costTime = (CFAbsoluteTimeGetCurrent() - self.startQueryTime) * 1000; 69 | } else if (self.willQueryTime != 0) { 70 | //页面内跳转H5计时 71 | costTime = (CFAbsoluteTimeGetCurrent() - self.willQueryTime) * 1000; 72 | } 73 | self.startQueryTime = 0; //时间戳计数清零 74 | self.willQueryTime = 0; 75 | 76 | [dataDict setObject:[NSNumber numberWithInt:costTime] forKey:@"loadTime"]; 77 | [dataDict setObject:[NSNumber numberWithInt:(event == HLLOfflineWebDataReportEventWebviewLoadSuccess) ? 0 : -1] 78 | forKey:@"loadResult"]; // 0成功,-1 失败 79 | [dataDict setObject:errMsg ?: @"" forKey:@"errMsg"]; 80 | [dataDict setObject:[NSNumber numberWithLong:errCode] forKey:@"errCode"]; 81 | [dataDict setObject:[NSNumber numberWithLong:self.httpResponseCode] forKey:@"httpCode"]; 82 | if ([self.originURL isFileURL]) { 83 | [dataDict setObject:[NSNumber numberWithInt:1] forKey:@"isOffweb"]; 84 | } else { 85 | [dataDict setObject:[NSNumber numberWithInt:0] forKey:@"isOffweb"]; 86 | } 87 | [dataDict setObject:self.bisName ?: @"" forKey:@"bisName"]; 88 | 89 | [HLLOfflineWebPackage getInstance].reportBlock(@"offweb_client_load_time", dataDict); 90 | 91 | //后续为实时监控上报数据。 92 | NSString *fragment = self.originURL.fragment; 93 | NSString *appendstring = nil; 94 | if (fragment != nil && [fragment length] > 0) { 95 | NSRange range; 96 | range = [fragment rangeOfString:@"?"]; 97 | if (range.location == NSNotFound) { 98 | appendstring = fragment; 99 | } else { 100 | appendstring = [fragment substringToIndex:range.location]; 101 | } 102 | } 103 | NSString *urlWithFragment = simpleUrl; 104 | if (appendstring != nil && [appendstring length] != 0) { 105 | urlWithFragment = [urlWithFragment stringByAppendingFormat:@"#%@", appendstring]; 106 | } 107 | 108 | NSMutableDictionary *monitorLables = [NSMutableDictionary dictionary]; 109 | [monitorLables setObject:self.originURL.scheme ?: @"" forKey:@"scheme"]; 110 | [monitorLables setObject:urlWithFragment ?: @"" forKey:@"url"]; 111 | [monitorLables setObject:self.bisName ?: @"" forKey:@"bisName"]; 112 | 113 | if (event == HLLOfflineWebDataReportEventWebviewLoadSuccess && self.httpResponseCode >= 400) { 114 | // 404错误会统计到网络成功里 115 | [monitorLables setObject:[NSString stringWithFormat:@"HTTP%ld", self.httpResponseCode] forKey:@"result"]; 116 | } else { 117 | [monitorLables setObject:[NSString stringWithFormat:@"%ld", errCode] forKey:@"result"]; 118 | } 119 | 120 | [HLLOfflineWebPackage getInstance].monitorBlock(HLLOfflineWebMonitorTypeSummary, @"webviewLoadTime", 121 | costTime * 1.0, monitorLables); 122 | 123 | self.httpResponseCode = 0; // http返回码清零 124 | } 125 | } 126 | 127 | @end 128 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebPackage/HLLOfflineWebPackage.h: -------------------------------------------------------------------------------- 1 | // 2 | // OfflineWebPackage.h 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/7/19. 6 | // 离线包核心模块。包含离线包查询、下载、更新、降级功能。 7 | 8 | #import "HLLOfflineWebConst.h" 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 离线包功能对外接入的主类 14 | @interface HLLOfflineWebPackage : NSObject 15 | @property (nonatomic, copy) NSString *env; ///< 设置后端环境,debug需要,release包不需要设置 16 | @property (nonatomic, assign) HLLOfflineWebDownloadType downloadSDKType; ///< 设置下载sdk方式,默认不启用断点续传 17 | @property (nonatomic, assign) BOOL disalbleFlag;///< 全局屏蔽,开启后离线包的更新和加载逻辑暂时实效。 18 | 19 | ///获取单例接口 20 | /// @return 返回单例对象 21 | + (HLLOfflineWebPackage *)getInstance; 22 | 23 | /// 设置客户端版本 24 | /// @param version 客户端版本号 25 | - (void)setAppVersion:(NSString *)version; 26 | 27 | /// 提取当前url对应的本地离线包业务名 28 | /// @param urlStr 在线网页的url串 29 | /// @return 离线业务名,没有时返回nil 30 | - (NSString *_Nullable)getOffWebBisName:(NSString *)urlStr; 31 | 32 | /// 获取当前url对应的本地离线包中的index.html路径 33 | /// @param webUrl 在线网页的url 34 | /// @return 对应的本地index文件路径,没有时返回nil 35 | - (NSURL *_Nullable)getFileURL:(NSURL *)webUrl; 36 | 37 | /// 离线包更新检查接口 38 | /// @param bisName 离线包业务名 39 | /// @param resultBlock 结果回调 40 | - (void)checkUpdate:(NSString *)bisName result:(HLLOfflineWebResultBlock)resultBlock; 41 | 42 | /// 将某个离线加入黑名单,暂时禁用更新和加载 43 | /// @param bisName 离线包业务名 44 | - (void)addToDisableList:(NSString *)bisName; 45 | 46 | @end 47 | 48 | NS_ASSUME_NONNULL_END 49 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebPackage/HLLOfflineWebPackage.m: -------------------------------------------------------------------------------- 1 | // 2 | // OfflineWebPackage.m 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/7/19. 6 | // 7 | 8 | #import "HLLOfflineWebDownloadMgr.h" 9 | #import "HLLOfflineWebFileMgr.h" 10 | #import "HLLOfflineWebFileUtil.h" 11 | #import "HLLOfflineWebPackage+callbacks.h" 12 | #import "HLLOfflineWebPackage.h" 13 | #import 14 | 15 | #define CHECK_UPDATE_URL @"http://localhost:5555/offweb" 16 | 17 | @interface HLLOfflineWebPackage () { 18 | } 19 | 20 | @property (nonatomic, copy) NSString *appVersion; 21 | @property (nonatomic, strong) HLLOfflineWebDownloadMgr *downloadMgr; 22 | @property (nonatomic, strong) NSMutableSet *disableBisList; 23 | 24 | @end 25 | 26 | @implementation HLLOfflineWebPackage 27 | 28 | + (HLLOfflineWebPackage *)getInstance { 29 | static dispatch_once_t onceToken; 30 | static HLLOfflineWebPackage *instance = nil; 31 | dispatch_once(&onceToken, ^{ 32 | instance = [[self alloc] init]; 33 | }); 34 | return instance; 35 | } 36 | 37 | - (void)callbackMainThread:(HLLOfflineWebResultBlock)block Ret:(HLLOfflineWebResultEvent)ret Msg:(NSString *)msg { 38 | if ([[NSThread currentThread] isMainThread]) { 39 | if (block) { 40 | block(ret, msg); 41 | } 42 | } else { 43 | dispatch_async(dispatch_get_main_queue(), ^{ 44 | if (block) { 45 | block(ret, msg); 46 | } 47 | }); 48 | } 49 | } 50 | 51 | - (instancetype)init { 52 | self = [super init]; 53 | if (self) { 54 | _appVersion = @"0.0.0"; 55 | self.downloadMgr = [[HLLOfflineWebDownloadMgr alloc] init]; 56 | self.disalbleFlag = NO; 57 | self.disableBisList = [[NSMutableSet alloc] init]; 58 | } 59 | return self; 60 | } 61 | 62 | - (NSString *)getOffWebBisName:(NSString *)str { 63 | if (self.disalbleFlag) { 64 | return nil; 65 | } 66 | 67 | NSURL *nsurl = [NSURL URLWithString:str]; 68 | NSString *paramsStr = nsurl.query; 69 | NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary]; 70 | NSArray *paramArray = [paramsStr componentsSeparatedByString:@"&"]; 71 | for (NSString *param in paramArray) { 72 | if (param && param.length) { 73 | NSArray *parArr = [param componentsSeparatedByString:@"="]; 74 | if (parArr.count == 2) { 75 | [paramsDict setObject:parArr[1] forKey:parArr[0]]; 76 | } 77 | } 78 | } 79 | if (paramsDict[@"offweb"] && [self.disableBisList containsObject:paramsDict[@"offweb"]]) { 80 | return nil; 81 | } 82 | 83 | return paramsDict[@"offweb"]; 84 | } 85 | 86 | - (NSURL *)getFileURL:(NSURL *)webUrl { 87 | NSString *query = webUrl.query; 88 | NSString *host = webUrl.host; 89 | NSString *bisName = [self getOffWebBisName:webUrl.absoluteString]; 90 | [HLLOfflineWebFileMgr doNewFolder2CurFolder:bisName]; 91 | NSString *filePath = [HLLOfflineWebFileMgr getPath:bisName]; 92 | NSFileManager *fileManager = [NSFileManager defaultManager]; 93 | if (![fileManager fileExistsAtPath:filePath]) { 94 | NSLog(@"offweb本地离线包不存在"); 95 | self.logBlock(HLLOfflineWebLogLevelWarning, bisName, @"no local offweb file!"); 96 | return nil; 97 | } 98 | 99 | filePath = [NSString stringWithFormat:@"file://%@", filePath]; 100 | if (query && [query length] > 0 && ![query containsString:@"offweb_host="]) { 101 | query = [query stringByAppendingFormat:@"&offweb_host=%@", host]; 102 | } 103 | if (webUrl.fragment) { 104 | filePath = [filePath stringByAppendingFormat:@"?%@#%@", query, webUrl.fragment]; //增加frament参数 105 | } else { 106 | //没有frament参数时就不拼 107 | filePath = [filePath stringByAppendingFormat:@"?%@", query]; 108 | } 109 | 110 | return [NSURL URLWithString:filePath]; 111 | } 112 | 113 | - (void)checkUpdate:(NSString *)bisName result:(HLLOfflineWebResultBlock)resultBlock { 114 | if (self.disalbleFlag) { 115 | resultBlock(HLLOfflineWebDisable, @"disable ALL offweb!"); 116 | return; 117 | } 118 | 119 | if ([self.disableBisList containsObject:bisName]) { 120 | resultBlock(HLLOfflineWebDisable, @"disable current!"); 121 | return; 122 | } 123 | 124 | if (!bisName || bisName.length == 0) { 125 | resultBlock(HLLOfflineWebParseError, @"bisName is nil"); 126 | return; 127 | } 128 | 129 | NSMutableDictionary *reportdict = [NSMutableDictionary dictionary]; 130 | [reportdict setValue:bisName forKey:@"bisName"]; 131 | CFAbsoluteTime startQueryTime = CFAbsoluteTimeGetCurrent(); 132 | NSString *offwebcurVer = [HLLOfflineWebFileMgr getDiskCurVersion:bisName]; 133 | // 构造请求URL 134 | NSString *urlStr = [[NSString alloc] initWithFormat:@"%@?bisName=%@&os=iOS&offlineZipVer=%@&clientVersion=%@", 135 | CHECK_UPDATE_URL, bisName, offwebcurVer, self.appVersion]; 136 | if (self.env && [self.env length] > 0) { 137 | urlStr = [urlStr stringByAppendingFormat:@"&env=%@", self.env]; 138 | } 139 | NSURL *url = [NSURL URLWithString:urlStr]; 140 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 141 | NSURLSession *session = [NSURLSession sharedSession]; 142 | NSURLSessionDataTask *dataTask = [session 143 | dataTaskWithRequest:request 144 | completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) { 145 | // 解析数据 146 | int queryCostTime = (CFAbsoluteTimeGetCurrent() - startQueryTime) * 1000; 147 | [reportdict setObject:[NSNumber numberWithInt:queryCostTime] forKey:@"queryTime"]; 148 | [HLLOfflineWebFileMgr deleteOldFolder:bisName]; 149 | if (!error) { 150 | [self parseCheckRspData:bisName data:data resultBlock:resultBlock reportdict:reportdict]; 151 | } else { 152 | [self callbackMainThread:resultBlock Ret:HLLOfflineWebQueryError Msg:error.description]; 153 | self.logBlock(HLLOfflineWebLogLevelError, bisName, 154 | [NSString stringWithFormat:@"checkupate network err msg:%@", error.description]); 155 | [reportdict setValue:[NSNumber numberWithInt:-1] forKey:@"queryResult"]; 156 | [reportdict setValue:error.description forKey:@"queryMsg"]; 157 | [self downloadDatareport:1 158 | msg:@"query fail,no download" 159 | downloadTime:0 160 | dict:reportdict]; //查询失败,无需下载 161 | } 162 | }]; 163 | 164 | [dataTask resume]; 165 | self.logBlock(HLLOfflineWebLogLevelInfo, bisName, [NSString stringWithFormat:@"check update,url: %@", urlStr]); 166 | 167 | 168 | } 169 | 170 | //解析返回数据 171 | - (void)parseCheckRspData:(NSString *)bisName 172 | data:(NSData *)data 173 | resultBlock:(HLLOfflineWebResultBlock)resultBlock 174 | reportdict:(NSMutableDictionary *)reportdict { 175 | if (!data) { 176 | [self callbackMainThread:resultBlock Ret:HLLOfflineWebQueryError Msg:@"data = nil"]; 177 | self.logBlock(HLLOfflineWebLogLevelError, bisName, @"data = nil"); 178 | return; 179 | } 180 | 181 | NSString *rspStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 182 | 183 | self.logBlock(HLLOfflineWebLogLevelInfo, bisName, rspStr); 184 | 185 | NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil]; 186 | NSString *rspbisName = [dict objectForKey:@"bisName"]; 187 | int result = [[dict objectForKey:@"result"] intValue]; 188 | NSString *url = [dict objectForKey:@"url"]; 189 | NSString *version = [dict objectForKey:@"version"]; // offlineZipVer 190 | int refreshMode = [[dict objectForKey:@"refreshMode"] intValue]; 191 | 192 | [reportdict setValue:[NSNumber numberWithInt:0] forKey:@"queryResult"]; 193 | [reportdict setValue:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] forKey:@"queryMsg"]; 194 | 195 | if ([bisName isEqualToString:rspbisName]) { 196 | if (result > 0) { 197 | NSString *newVersion = [HLLOfflineWebFileMgr getDiskNewVersion:bisName]; 198 | if ([newVersion isEqualToString:version]) { 199 | //如果本地已经有下载好的版本,不重复下载 200 | if (refreshMode == 0) { 201 | [self callbackMainThread:resultBlock 202 | Ret:HLLOfflineWebRefreshPackageLater 203 | Msg:@"本地已有下好版本,下次生效"]; 204 | self.logBlock(HLLOfflineWebLogLevelInfo, bisName, @"本地已有下好版本,下次生效"); 205 | } else if (refreshMode == 1) { 206 | [self callbackMainThread:resultBlock 207 | Ret:HLLOfflineWebRefreshPackageNow 208 | Msg:@"本地已有下好版本,立刻生效"]; 209 | self.logBlock(HLLOfflineWebLogLevelInfo, bisName, @"本地已有下好版本,立刻生效"); 210 | } 211 | 212 | [self downloadDatareport:1 213 | msg:@"NewFolder == online version" 214 | downloadTime:0 215 | dict:reportdict]; //本地已有,无需下载 216 | return; 217 | } 218 | 219 | [self dowloadAndUnzip:bisName 220 | Version:version 221 | Url:url 222 | result:^(HLLOfflineWebResultEvent result, NSString *msg) { 223 | if (result == HLLOfflineWebUnzipSuccess) { 224 | if (refreshMode == 0) { 225 | [self callbackMainThread:resultBlock 226 | Ret:HLLOfflineWebRefreshPackageLater 227 | Msg:@"解压成功,下次生效"]; 228 | self.logBlock(HLLOfflineWebLogLevelInfo, bisName, @"unzip success.act Next"); 229 | } else if (refreshMode == 1) { 230 | [self callbackMainThread:resultBlock 231 | Ret:HLLOfflineWebRefreshPackageNow 232 | Msg:@"解压成功,马上生效"]; 233 | self.logBlock(HLLOfflineWebLogLevelInfo, bisName, @"unzip success.act Now"); 234 | } 235 | } else { 236 | [self callbackMainThread:resultBlock Ret:result Msg:msg]; 237 | self.logBlock(HLLOfflineWebLogLevelError, bisName, @"unzip fail!"); 238 | } 239 | } 240 | Dict:reportdict]; 241 | 242 | } else if (result == 0) { 243 | self.logBlock(HLLOfflineWebLogLevelInfo, bisName, @"no new zip"); 244 | [self callbackMainThread:resultBlock Ret:HLLOfflineWebNoUpdate Msg:@"线上无新包"]; 245 | [self downloadDatareport:1 msg:@"no new zip" downloadTime:0 dict:reportdict]; //本地已有,无需下载 246 | } else if (result == -1) { 247 | [HLLOfflineWebFileMgr deleteDiskCache:bisName]; 248 | self.logBlock(HLLOfflineWebLogLevelWarning, bisName, @"disabel offweb,delete local folder"); 249 | [self callbackMainThread:resultBlock Ret:HLLOfflineWebRefreshOnlineWebNow Msg:@"disable offlineWeb"]; 250 | [self downloadDatareport:1 msg:@"disable offweb" downloadTime:0 dict:reportdict]; //本地已有,无需下载 251 | } 252 | } else { 253 | [self callbackMainThread:resultBlock Ret:HLLOfflineWebQueryError Msg:@"bisName is not right"]; 254 | self.logBlock(HLLOfflineWebLogLevelError, bisName, @"bisName is not right"); 255 | [self downloadDatareport:1 msg:@"bisName is not right" downloadTime:0 dict:reportdict]; //本地已有,无需下载 256 | } 257 | } 258 | 259 | - (void)dowloadAndUnzip:(NSString *)bisName 260 | Version:(NSString *)version 261 | Url:(NSString *)urlStr 262 | result:(HLLOfflineWebResultBlock)resultBlock 263 | Dict:(NSMutableDictionary *)reportdict { 264 | __weak typeof(self) weakSelf = self; 265 | CFAbsoluteTime startDownloadTime = CFAbsoluteTimeGetCurrent(); 266 | [self.downloadMgr 267 | downloadZip:bisName 268 | version:version 269 | url:urlStr 270 | result:^(HLLOfflineWebResultEvent result, NSString *msg) { 271 | __strong typeof(self) strongself = weakSelf; 272 | int downloadCostTime = (int)(CFAbsoluteTimeGetCurrent() - startDownloadTime) * 1000; 273 | if (result == HLLOfflineWebDownloadSuccess) { 274 | NSString *zipPath = msg; 275 | int fileSize = [HLLOfflineWebFileUtil getFileSize:zipPath]; 276 | CFAbsoluteTime startUnZipTime = CFAbsoluteTimeGetCurrent(); 277 | [HLLOfflineWebFileMgr 278 | doZiptoNewFolder:bisName 279 | Zip:zipPath 280 | Result:^(HLLOfflineWebResultEvent zipResult, NSString *zipMsg) { 281 | if (!strongself) { 282 | return; 283 | } 284 | 285 | strongself.logBlock( 286 | zipResult == HLLOfflineWebUnzipSuccess ? HLLOfflineWebLogLevelInfo 287 | : HLLOfflineWebLogLevelError, 288 | bisName, [NSString stringWithFormat:@"zip result:%@", zipMsg]); 289 | int unzipCostTime = 290 | (int)(CFAbsoluteTimeGetCurrent() - startUnZipTime) * 1000; 291 | [reportdict setValue:[NSNumber numberWithInt:(zipResult == 292 | HLLOfflineWebUnzipSuccess) 293 | ? 0 294 | : -1] 295 | forKey:@"unzipResult"]; 296 | [reportdict setValue:[NSNumber numberWithInt:unzipCostTime] 297 | forKey:@"unzipTime"]; 298 | [reportdict setValue:zipMsg forKey:@"unzipMsg"]; 299 | [reportdict setValue:[NSNumber numberWithInt:fileSize] forKey:@"zipSize"]; 300 | 301 | [strongself downloadDatareport:0 302 | msg:@"download success" 303 | downloadTime:downloadCostTime 304 | dict:reportdict]; 305 | if (strongself.downloadSDKType == HLLOfflineWebDownloadTypeSystemAPI) { 306 | NSFileManager *fileManager = [NSFileManager defaultManager]; 307 | [fileManager removeItemAtPath:zipPath error:nil]; // del zip包 308 | } else { 309 | // [weakSelf.downloadMgr delSDKZip:urlStr]; 310 | } 311 | strongself.logBlock(HLLOfflineWebLogLevelInfo, bisName, @"del zip"); 312 | resultBlock(zipResult, zipMsg); 313 | }]; 314 | } else { 315 | self.logBlock(HLLOfflineWebLogLevelError, bisName, @"download fail!"); 316 | resultBlock(result, msg); 317 | [self downloadDatareport:-1 318 | msg:@"download fail" 319 | downloadTime:downloadCostTime 320 | dict:reportdict]; 321 | } 322 | } 323 | downloadSDKType:self.downloadSDKType]; //下载sdk选择 324 | } 325 | 326 | - (void)downloadDatareport:(int)result 327 | msg:(NSString *)msg 328 | downloadTime:(int)downloadTime 329 | dict:(NSMutableDictionary *)reportdict { 330 | [reportdict setValue:[NSNumber numberWithInt:result] forKey:@"downloadResult"]; 331 | [reportdict setValue:[NSNumber numberWithInt:downloadTime] forKey:@"downloadTime"]; 332 | [reportdict setValue:msg forKey:@"downloadMsg"]; 333 | if (result == -1 || result == 1) { 334 | [reportdict setValue:[NSNumber numberWithInt:1] forKey:@"unzipResult"]; 335 | [reportdict setValue:[NSNumber numberWithInt:0] forKey:@"unzipTime"]; 336 | [reportdict setValue:@"" forKey:@"unzipMsg"]; 337 | [reportdict setValue:[NSNumber numberWithInt:0] forKey:@"zipSize"]; 338 | self.reportBlock(@"offweb_cost_time", reportdict); 339 | } else if (result == 0) { 340 | self.reportBlock(@"offweb_cost_time", reportdict); 341 | } 342 | } 343 | 344 | - (void)addToDisableList:(NSString *)bisName { 345 | [self.disableBisList addObject:bisName]; 346 | } 347 | 348 | #pragma mark - blocks safe lazy load 349 | 350 | - (HLLOfflineWebLogBlock)logBlock { 351 | if (!_logBlock) { 352 | _logBlock = ^(HLLOfflineWebLogLevel level, NSString *keyword, NSString *message) { 353 | NSLog(@"offweblog:%d:%@:%@", (int)level, keyword, message); 354 | }; 355 | } 356 | return _logBlock; 357 | } 358 | - (HLLOfflineWebReportBlock)reportBlock { 359 | if (!_reportBlock) { 360 | _reportBlock = ^(NSString *event, NSDictionary *dict) { 361 | NSLog(@"report event: %@ dict:%@", event, dict); 362 | }; 363 | } 364 | return _reportBlock; 365 | } 366 | 367 | - (HLLOfflineWebMonitorBlock)monitorBlock { 368 | if (!_monitorBlock) { 369 | _monitorBlock = ^(HLLOfflineWebMonitorType type, NSString *key, CGFloat value, NSDictionary *lables) { 370 | NSLog(@"offwebMonitor:%d:%@:%f", (int)type, key, value); 371 | }; 372 | } 373 | return _monitorBlock; 374 | } 375 | 376 | @end 377 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebUtils/HLLOfflineWebFileUtil.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebFileUtil.h 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/7/20. 6 | // 7 | 8 | #import "HLLOfflineWebConst.h" 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 文件操作工具类 14 | @interface HLLOfflineWebFileUtil : NSObject 15 | 16 | /// zip文件解压 17 | /// @param src zip文件本地路径 18 | /// @param dst 解压的目的path 19 | /// @param result 结果回调 20 | + (void)unzipLocalFile:(NSString *)src dst:(NSString *)dst result:(HLLOfflineWebResultBlock)result; 21 | 22 | /// 获取指定路径的文件的存储空间大小 23 | /// @param path 对应文件的路径 24 | /// @return 如果path文件不存在则返回-1, 否则返回其存储空间大小,单位为:kb 25 | /// @warning 此方法只能获取单个实体文件的存储空间,不能正确统计一个目录中的总存储空间 26 | + (CGFloat)getFileSize:(NSString *)path; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/OfflineWebUtils/HLLOfflineWebFileUtil.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebFileUtil.m 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/7/20. 6 | // 7 | 8 | #import "HLLOfflineWebFileUtil.h" 9 | #import 10 | #import 11 | 12 | @implementation HLLOfflineWebFileUtil 13 | 14 | + (void)unzipLocalFile:(NSString *)src dst:(NSString *)dst result:(HLLOfflineWebResultBlock)result { 15 | [SSZipArchive unzipFileAtPath:src 16 | toDestination:dst 17 | preserveAttributes:nil 18 | overwrite:YES 19 | nestedZipLevel:0 20 | password:nil 21 | error:nil 22 | delegate:nil 23 | progressHandler:^(NSString *_Nonnull entry, unz_file_info zipInfo, long entryNumber, long total) { 24 | //解压进度回调,暂忽略 25 | } 26 | completionHandler:^(NSString *_Nonnull path, BOOL succeeded, NSError *_Nullable error) { 27 | //解压结果回调 28 | if (succeeded) { 29 | result(HLLOfflineWebUnzipSuccess, @"unzip success"); 30 | } else { 31 | result(HLLOfflineWebUnzipError, [error description]); 32 | } 33 | }]; 34 | } 35 | 36 | + (CGFloat)getFileSize:(NSString *)path { 37 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 38 | CGFloat filesize = -1.0; 39 | if ([fileManager fileExistsAtPath:path]) { 40 | NSDictionary *fileDic = [fileManager attributesOfItemAtPath:path error:nil]; //获取文件的属性 41 | unsigned long long size = [[fileDic objectForKey:NSFileSize] longLongValue]; 42 | filesize = 1.0 * size / 1024; 43 | } 44 | return filesize; 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/Private/HLLOfflineWebDownloadMgr.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOffWebDownload.h 3 | // HLLOfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/8/19. 6 | //离线包下载。可替换成其他下载SDK 7 | 8 | #import "HLLOfflineWebConst.h" 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 离线包文件下载管理类,仅供SDK内部逻辑调度使用,通常在 HLLOfflineWebPackage 类内部逻辑调用 14 | /// @note 此类相关代码,若有不明可以咨询 15 | @interface HLLOfflineWebDownloadMgr : NSObject 16 | 17 | /// 下载离线包文件 18 | /// @param bisName 业务名字符串 19 | /// @param version 版本串 20 | /// @param urlStr 下载的链接 21 | /// @param resultBlock 结果回调 22 | /// @param downloadSDKType 下载方式类型 23 | - (void)downloadZip:(NSString *)bisName 24 | version:(NSString *)version 25 | url:(NSString *)urlStr 26 | result:(HLLOfflineWebResultBlock)resultBlock 27 | downloadSDKType:(HLLOfflineWebDownloadType)downloadSDKType; 28 | 29 | @end 30 | 31 | NS_ASSUME_NONNULL_END 32 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/Private/HLLOfflineWebDownloadMgr.m: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOffWebDownload.m 3 | // HLLOfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/8/19. 6 | // 7 | 8 | #import "HLLOfflineWebDownloadMgr.h" 9 | #import "HLLOfflineWebPackage+callbacks.h" 10 | 11 | /// 内部用的便捷的log方法 12 | NS_INLINE void HLLOfflineWebDownloadMgr_offlineWebLog(HLLOfflineWebLogLevel level, NSString *keyword, NSString *message) { 13 | [HLLOfflineWebPackage getInstance].logBlock(level, keyword, message); 14 | }; 15 | 16 | @interface HLLOfflineWebDownloadMgr () { 17 | } 18 | 19 | @property (nonatomic, strong) NSMutableDictionary *downloadingDict; 20 | @end 21 | 22 | @implementation HLLOfflineWebDownloadMgr 23 | 24 | - (instancetype)init { 25 | self = [super init]; 26 | if (self) { 27 | self.downloadingDict = [NSMutableDictionary dictionary]; 28 | } 29 | return self; 30 | } 31 | 32 | - (void)downloadZip:(NSString *)bisName 33 | version:(NSString *)version 34 | url:(NSString *)urlStr 35 | result:(HLLOfflineWebResultBlock)resultBlock 36 | downloadSDKType:(HLLOfflineWebDownloadType)downloadSDKType { 37 | 38 | [self downloadUseAPI:bisName version:version url:urlStr result:resultBlock]; 39 | } 40 | 41 | - (void)downloadUseAPI:(NSString *)bisName 42 | version:(NSString *)version 43 | url:(NSString *)urlStr 44 | result:(HLLOfflineWebResultBlock)resultBlock { 45 | if (!bisName || bisName.length == 0) { 46 | resultBlock(HLLOfflineWebParseError, @"bisName is nil"); 47 | return; 48 | } 49 | 50 | if (!urlStr || [urlStr length] == 0) { 51 | resultBlock(HLLOfflineWebParseError, @"url is invalid"); 52 | return; 53 | } 54 | 55 | NSURL *url = [NSURL URLWithString:urlStr]; 56 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 57 | NSURLSession *session = [NSURLSession sharedSession]; 58 | if ([self.downloadingDict objectForKey:urlStr] == nil) { 59 | self.downloadingDict[urlStr] = @"1"; 60 | NSURLSessionDownloadTask *downloadTask = 61 | [session downloadTaskWithRequest:request 62 | completionHandler:^(NSURL *_Nullable location, NSURLResponse *_Nullable response, 63 | NSError *_Nullable error) { 64 | [self.downloadingDict removeObjectForKey:urlStr]; 65 | if (!error) { 66 | HLLOfflineWebDownloadMgr_offlineWebLog( 67 | HLLOfflineWebLogLevelInfo, bisName, 68 | [NSString stringWithFormat:@"download sucess:%@", urlStr]); 69 | resultBlock(HLLOfflineWebDownloadSuccess, [location path]); 70 | } else { 71 | HLLOfflineWebDownloadMgr_offlineWebLog( 72 | HLLOfflineWebLogLevelInfo, bisName, 73 | [NSString stringWithFormat:@"download sucess:%@", urlStr]); 74 | resultBlock(HLLOfflineWebDownloadError, error.description); 75 | } 76 | }]; 77 | [downloadTask resume]; 78 | 79 | } else { 80 | HLLOfflineWebDownloadMgr_offlineWebLog(HLLOfflineWebLogLevelWarning, bisName, 81 | @"此版本已在下载中,不再重复下载"); 82 | resultBlock(HLLOfflineWebDownloading, @"offweb is downloading"); 83 | } 84 | } 85 | 86 | @end 87 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/Private/HLLOfflineWebFileMgr.h: -------------------------------------------------------------------------------- 1 | // 2 | // OfflineWebFileMgr.h 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/8/11. 6 | // 离线包相关的文件操作 7 | 8 | #import "HLLOfflineWebConst.h" 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 离线包本地文件管理类,主要在 HLLOfflineWebPackage 类内部逻辑调用 14 | @interface HLLOfflineWebFileMgr : NSObject 15 | 16 | + (void)doZiptoNewFolder:(NSString *)bisName Zip:(NSString *)zipPath Result:(HLLOfflineWebResultBlock)resultBlock; 17 | + (void)deleteOldFolder:(NSString *)bisName; 18 | + (BOOL)doNewFolder2CurFolder:(NSString *)bisName; 19 | + (BOOL)deleteDiskCache; /// 删除所有离线包缓存 20 | + (BOOL)deleteDiskCache:(NSString *)bisName; /// 删除某个业务的离线包 21 | 22 | + (NSString *)getDiskCurVersion:(NSString *)bisName; /// 内部实现有IO操作读文件 23 | + (NSString *)getDiskNewVersion:(NSString *)bisName; /// 内部实现有IO操作读文件 24 | 25 | + (NSString *)getOfflineWebStorePath:(NSString *)bisName; 26 | + (NSString *)getPath:(NSString *)bisName; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/Private/HLLOfflineWebFileMgr.m: -------------------------------------------------------------------------------- 1 | // 2 | // OfflineWebFileMgr.m 3 | // OfflineWebPackage 4 | // 5 | // Created by 货拉拉 on 2021/8/11. 6 | // 7 | 8 | #import "HLLOfflineWebFileMgr.h" 9 | #import "HLLOfflineWebFileUtil.h" 10 | #import "HLLOfflineWebPackage+callbacks.h" 11 | 12 | /// log方法宏定义 13 | NS_INLINE void HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevel level, NSString *keyword, NSString *message) { 14 | [HLLOfflineWebPackage getInstance].logBlock(level, keyword, message); 15 | }; 16 | 17 | @implementation HLLOfflineWebFileMgr 18 | 19 | + (NSString *)getOfflineWebStorePath:(NSString *)bisName { 20 | NSString *documentPath = 21 | NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 22 | NSString *path = [documentPath stringByAppendingString:[NSString stringWithFormat:@"/offlineWeb/%@", bisName]]; 23 | return path; 24 | } 25 | 26 | + (void)doZiptoNewFolder:(NSString *)bisName Zip:(NSString *)zipPath Result:(HLLOfflineWebResultBlock)resultBlock { 27 | 28 | NSFileManager *fileManager = [NSFileManager defaultManager]; 29 | //存储zip 30 | NSString *storePath = [self getOfflineWebStorePath:bisName]; 31 | NSString *newFolder = [storePath stringByAppendingString:@"/new"]; 32 | NSString *tempFolder = [storePath stringByAppendingString:@"/temp"]; 33 | NSString *curFolder = [storePath stringByAppendingString:@"/cur"]; 34 | 35 | if (![fileManager fileExistsAtPath:storePath]) { 36 | //解决该目录未创建,解压时报找不到指定目录错误的问题 37 | [fileManager createDirectoryAtPath:storePath withIntermediateDirectories:YES attributes:nil error:nil]; 38 | // create folder 39 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelWarning, bisName, @"create storePath"); 40 | } 41 | if ([fileManager fileExistsAtPath:tempFolder]) { 42 | //可能存在之前解压一半的情况 43 | [fileManager removeItemAtPath:tempFolder error:nil]; // del temp 44 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelWarning, bisName, @"del temp folder"); 45 | } 46 | 47 | [HLLOfflineWebFileUtil 48 | unzipLocalFile:zipPath 49 | dst:tempFolder 50 | result:^(HLLOfflineWebResultEvent result, NSString *message) { 51 | if (result == HLLOfflineWebUnzipSuccess) { 52 | if (![fileManager fileExistsAtPath:curFolder]) { 53 | // curFolder不存在 54 | [fileManager moveItemAtPath:tempFolder toPath:curFolder error:nil]; // temp->cur 55 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelInfo, bisName, @"tmp->cur"); 56 | } else { 57 | [fileManager removeItemAtPath:newFolder error:nil]; // del new 58 | [fileManager moveItemAtPath:tempFolder toPath:newFolder error:nil]; // temp->new 59 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelInfo, bisName, @"temp->new"); 60 | } 61 | } 62 | 63 | resultBlock(result, message); 64 | }]; 65 | } 66 | 67 | + (void)deleteOldFolder:(NSString *)bisName { 68 | NSString *storePath = [self getOfflineWebStorePath:bisName]; 69 | NSString *oldFolder = [storePath stringByAppendingString:@"/old"]; 70 | NSFileManager *fileManager = [NSFileManager defaultManager]; 71 | if ([fileManager fileExistsAtPath:oldFolder]) { 72 | [fileManager removeItemAtPath:oldFolder error:nil]; //重命名之前删除old 73 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelWarning, bisName, @"del old folder"); 74 | } 75 | } 76 | 77 | + (BOOL)doNewFolder2CurFolder:(NSString *)bisName { 78 | NSFileManager *fileManager = [NSFileManager defaultManager]; 79 | NSString *storePath = [self getOfflineWebStorePath:bisName]; 80 | NSString *oldFolder = [storePath stringByAppendingString:@"/old"]; 81 | NSString *curFolder = [storePath stringByAppendingString:@"/cur"]; 82 | NSString *newFolder = [storePath stringByAppendingString:@"/new"]; 83 | 84 | if (![fileManager fileExistsAtPath:newFolder]) { 85 | //新目录没有直接返回 86 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelWarning, bisName, @"no new folder"); 87 | return NO; 88 | } 89 | 90 | if (![fileManager fileExistsAtPath:curFolder]) { 91 | //当前目录不存在,直接重命名 92 | NSError *renameError = nil; 93 | [fileManager moveItemAtPath:newFolder toPath:curFolder error:&renameError]; 94 | if (renameError) { 95 | //重命名失败 96 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelError, bisName, @"new ->cur folder fail"); 97 | return NO; 98 | } 99 | } else { 100 | NSError *renameError = nil; 101 | [self deleteOldFolder:bisName]; 102 | [fileManager moveItemAtPath:curFolder toPath:oldFolder error:&renameError]; // cur ->old 103 | if (renameError) { 104 | //文件夹占用中,下次更新 105 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelError, bisName, @"cur ->old folder fail"); 106 | return NO; 107 | } else { 108 | NSError *renameError2 = nil; 109 | [fileManager moveItemAtPath:newFolder toPath:curFolder error:&renameError2]; // new->cur 110 | if (renameError2) { 111 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelError, bisName, @"new->cur folder fail"); 112 | return NO; 113 | } 114 | } 115 | } 116 | 117 | return YES; 118 | } 119 | 120 | + (BOOL)deleteDiskCache { 121 | NSFileManager *fileManager = [NSFileManager defaultManager]; 122 | NSString *documentPath = 123 | NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 124 | NSString *path = [documentPath stringByAppendingString:[NSString stringWithFormat:@"/offlineWeb/"]]; 125 | NSError *removeError = nil; 126 | [fileManager removeItemAtPath:path error:&removeError]; 127 | if (!removeError) { 128 | return YES; 129 | } else { 130 | return NO; 131 | } 132 | } 133 | 134 | //删除某个业务的离线包 135 | + (BOOL)deleteDiskCache:(NSString *)bisName { 136 | NSString *path = [self getOfflineWebStorePath:bisName]; 137 | NSFileManager *fileManager = [NSFileManager defaultManager]; 138 | NSError *error = nil; 139 | [fileManager removeItemAtPath:path error:&error]; 140 | if (!error) { 141 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelWarning, bisName, @"del cache success"); 142 | return YES; 143 | } else { 144 | HLLOfflineWebFileMgr_offlineWebLog(HLLOfflineWebLogLevelError, bisName, @"del cache fail"); 145 | return NO; 146 | } 147 | } 148 | 149 | + (NSString *)getDiskCurVersion:(NSString *)bisName { 150 | NSFileManager *fileManager = [NSFileManager defaultManager]; 151 | NSString *storePath = [self getOfflineWebStorePath:bisName]; 152 | NSString *curVerPath = [storePath stringByAppendingString:@"/cur/.offweb.json"]; 153 | if ([fileManager fileExistsAtPath:curVerPath]) { 154 | NSData *jsonData = [[NSData alloc] initWithContentsOfFile:curVerPath]; 155 | if (jsonData == nil) { 156 | return @"0"; 157 | } 158 | 159 | NSError *error = nil; 160 | NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData 161 | options:NSJSONReadingMutableContainers 162 | error:&error]; 163 | if (!jsonData || error) { 164 | HLLOfflineWebFileMgr_offlineWebLog( 165 | HLLOfflineWebLogLevelError, bisName, 166 | [NSString stringWithFormat:@"getDiskCurVersion,json decode error. %@", error.description]); 167 | return @"0"; 168 | } else { 169 | return dict[@"ver"]; 170 | } 171 | } 172 | 173 | return @"0"; 174 | } 175 | 176 | + (NSString *)getDiskNewVersion:(NSString *)bisName { 177 | NSFileManager *fileManager = [NSFileManager defaultManager]; 178 | NSString *storePath = [self getOfflineWebStorePath:bisName]; 179 | NSString *curVerPath = [storePath stringByAppendingString:@"/new/.offweb.json"]; 180 | if ([fileManager fileExistsAtPath:curVerPath]) { 181 | NSData *jsonData = [[NSData alloc] initWithContentsOfFile:curVerPath]; 182 | if (jsonData == nil) { 183 | return @"0"; 184 | } 185 | 186 | NSError *error = nil; 187 | NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData 188 | options:NSJSONReadingMutableContainers 189 | error:&error]; 190 | if (!jsonData || error) { 191 | // DLog(@"JSON解码失败"); 192 | HLLOfflineWebFileMgr_offlineWebLog( 193 | HLLOfflineWebLogLevelError, bisName, 194 | [NSString stringWithFormat:@"getDiskNewVersion,json decode error. %@", error.description]); 195 | return @"0"; 196 | } else { 197 | return dict[@"ver"]; 198 | } 199 | } 200 | 201 | return @"0"; 202 | } 203 | 204 | + (NSString *)getPath:(NSString *)bisName { 205 | NSString *storePath = [self getOfflineWebStorePath:bisName]; 206 | NSString *curFolderPath = [storePath stringByAppendingString:@"/cur/index.html"]; 207 | return curFolderPath; 208 | } 209 | 210 | @end 211 | -------------------------------------------------------------------------------- /HLLOfflineWebVC/Classes/Private/HLLOfflineWebPackage+callbacks.h: -------------------------------------------------------------------------------- 1 | // 2 | // HLLOfflineWebPackage+callbacks.h 3 | // HLLOfflineWebVC 4 | // 5 | // Created by 货拉拉 on 2022/3/11. 6 | // 7 | 8 | #import "HLLOfflineWebConst.h" 9 | #import "HLLOfflineWebPackage.h" 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 内部使用的一些外部赋值的block回调 14 | @interface HLLOfflineWebPackage () 15 | 16 | @property (nonatomic, copy) HLLOfflineWebLogBlock logBlock; 17 | @property (nonatomic, copy) HLLOfflineWebReportBlock reportBlock; 18 | @property (nonatomic, copy) HLLOfflineWebMonitorBlock monitorBlock; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /Image/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Image/1.gif -------------------------------------------------------------------------------- /Image/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Image/1.jpg -------------------------------------------------------------------------------- /Image/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Image/2.gif -------------------------------------------------------------------------------- /Image/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Image/2.png -------------------------------------------------------------------------------- /Image/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Image/3.png -------------------------------------------------------------------------------- /Image/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HuolalaTech/HLLOfflineWebVC-iOS/55dca2a4bc1228714a6bfa3def80b150fc23959f/Image/title.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 32 | 33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 34 | 35 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | You must cause any modified files to carry prominent notices stating that You changed the files; and 37 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 38 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 39 | 40 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 41 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 42 | 43 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 44 | 45 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 46 | 47 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 48 | 49 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 50 | 51 | END OF TERMS AND CONDITIONS 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![license](https://img.shields.io/hexpm/l/plug.svg)](https://www.apache.org/licenses/LICENSE-2.0) 4 | ![Pod Version](https://img.shields.io/badge/pod-v1.0.0-green.svg) 5 | ![Platform](https://img.shields.io/badge/platform-iOS-blue.svg) 6 | ![Language](https://img.shields.io/badge/language-ObjectC-green.svg) 7 | [![wiki](https://img.shields.io/badge/Wiki-open-brightgreen.svg)](https://juejin.cn/post/7103348563479887885) 8 | 9 | > [中文文档](README_CN.md) | 10 | > [Introduction](https://juejin.cn/post/7103348563479887885) 11 | 12 | --- 13 | HLLOfflineWebVC is a lightweight and high-performance hybrid framework developed by HUOLALA mobile team, which is intended to improve the load speed of websites on mobile phone. It base on [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview/) at iOS system. 14 | HLLOfflineWebVC can cache html, css, js, png and other static resource on the disk. When the app load the web page, it directly load the resource from disk and reduce network request. You can get more details from the [article](https://juejin.cn/post/7103348563479887885). 15 | 16 | ## Before VS After Using HLLOfflineWebVC 17 | 18 | | |Before Using HLLOfflineWebVC | After Using HLLOfflineWebVC 19 | |--|:-------------------------:|:-------------------------: 20 | | Time Cost|2s | 1s 21 | |Movie| | 22 | 23 | 24 | 25 | ## Features 26 | - Safe and reliable: no hook and no private API,  three degrade strategy. 27 | - Easy to maintain: three layer structure and modular design. 28 | - Fully functional: it contains H5 offline resource managing, url and offline resource mapping config, data reporting, debug tool. 29 | 30 | 31 | ## Requirements 32 | - iOS 9.0 or later 33 | - Xcode 11.0 or later 34 | - CocoaPods 1.11.2 or later 35 | 36 | ## Get Started 37 | 1) Download the code. 38 | 2) Enter the 'Example' folder, enter command 'pod install' to install 3rd-party libraries. 39 | 3) Open the project 'HLLOfflineWebVC.xcworkspace' and built it by Xcode. If you complile and run the demo successfully, you will see the UI below: 40 | 41 | 42 | 43 | 44 | ## Communication 45 | - If you find a bug, open an issue. 46 | - If you have a feature request, open an issue. 47 | - If you want to contribute, submit a pull request. 48 | ## Architecture 49 | - OfflineWebPackage 50 | 51 | The core module of HLLOfflineWebVC. 52 | - OfflineWebDevTool 53 | 54 | An useful tool to Debug the offline web. 55 | - OfflineWebBisNameMatch 56 | 57 | Connect the web page with offline resource by config. 58 | - OfflineWebUtils 59 | 60 | An inner API used by other module. 61 | ## How To Use 62 | If you want to bring the code into your project, you need to do the following: 63 | 64 | ### Develop A HTTP Service 65 | 1) HTTP Request 66 | 67 | https://huolala.cn/queryOffline?clientType=iOS&clientVer=1.0.0&offlineZipVer=1.0.0&bisName=xx 68 | 69 |  requet data parameters description: 70 | 71 | | parameter name | parameter meaning | note | 72 | |------------------|-------------------------------|-------------------------------------| 73 | | clientType | operating system type | iOS,Android | 74 | | clientVersion | app version | eg: 1.0 | 75 | | bisName | unique identifier of your offline web page | eg: act3-offline-package-test | 76 | | offlineZipVer | the local offline web file version | 0 means no offline web cache | 77 | 78 | 79 | 80 |  respond data format is a json,parameters description: 81 | 82 | | parameter name | parameter meaning | note | 83 | |--------------|-------------|--------------------------------------------------------------------------------------------| 84 | | bisName | unique identifier of your offline web page | eg: act3-offline-package-test | 85 | | result | query result | -1: disable offline web, 0: no update, 1: has new version | 86 | | url | zip file download url | if no update, the url is null | 87 | | refreshMode |notify client how to update | 0:update next(default) 1:update immediately | 88 | | version | offline web pages version | eg: 25609-j56gfa | 89 | 90 | 2) Cross Origin 91 | 92 | When an offline web page make a network request, the origin is null,should modify your gateway or server to support. 93 | 94 | ### Modify HTML And JS File 95 | - use relative path, no absolute path. 96 | - cookie、local storage should support the situation that host is null. 97 | - add a file to describe the offline web version .the file name is ".offweb.json" and the content is: 98 | { "bisName": "xxx", "ver": "xxx" } 99 | 100 | ## Bring The Code Into Your Client 101 | ### Install Completely 102 | Install all the modules in your project. 103 | 1) Add the string " pod 'HLLOfflineWebVC' " in your pod file. 104 | 105 | 2) Call the initial function. 106 | ``` 107 | - (void)initOfflineWeb { 108 | NSDictionary *offwebConfigDict = DefaultWebPackageConfig; 109 | [HLLOfflineWebConfig setInitalParam:offwebConfigDict 110 | logBlock:^(HLLOfflineWebLogLevel level, NSString *keyword, NSString *message) { 111 | NSLog (@"use your log SDK :%d:%@:%@", (int) level, keyword, message); 112 | } 113 | reportBlock:^(NSString *event, NSDictionary *dict) { 114 | NSLog(@"data report:%@,%@", event, dict); 115 | } 116 | monitorBlock:^(HLLOfflineWebMonitorType type, NSString *key, CGFloat value, NSDictionary *lables) { 117 | NSLog(@"use your monitor sdk "); 118 | 119 | } 120 | env:@"prd" 121 | appversion:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]; 122 | } 123 | 124 | ``` 125 | 3) Config the dictionary. 126 | 127 | "OffwebConfigDict" is a dictionary from json, configure the degrade strategy, auto add offweb parameters to url: 128 | ``` 129 | { 130 | "switch": 1, 131 | "predownloadlist": ["uappweb-offline"], 132 | "disablelist": [], 133 | "rules": [{ 134 | "host": ["test1.xxx.com", "test2.xxx.com"], 135 | "path": ["/uapp"], 136 | "offweb": "uappweb" 137 | } 138 | ] 139 | } 140 | ``` 141 | | parameter name | parameter meaning | required or optional | 142 | |--------------|-------------|--------------------------------------------------------------------------------------------| 143 | | switch | 1 open,0 close | required | 144 | | predownloadlist | If you need download an offline web page, add the business name | optional | 145 | | disablelist | disable some offline web page | optional | 146 | | rules |when the host and path match the rules, webview will add 'offweb=uappweb' to you url | optional | 147 | 4) Modify webview container. 148 | 149 | Implement the function bellow in your basic webview container. Then edit the parent class of HLLOfflineWebVC. 150 | ``` 151 | - (BOOL)webview:(WKWebView *)webview shouldStartLoadWithRequest:(NSURLRequest *)request { 152 | return YES; 153 | } 154 | ``` 155 | Declare WKWebview delegate ` ` in the head file. 156 | 157 | ### Install Core Module 158 | 159 | Just install the core module that not contains 'HLLOfflineWebVC', the CocoaPods command is: pod 'HLLOfflineWebVC/OfflineWebPackage' . 160 | The main API : 161 | 1) Check Update API 162 | ``` 163 | //@param bisName: unique identifier of your offline web page 164 | //@param resultBlock:the result block 165 | 166 | - (void)checkUpdate:(NSString *)bisName result:(HLLOfflineWebResultBlock)resultBlock; 167 | ``` 168 | 2) Get the Local File Path of Offline Web 169 | ``` 170 | //@param webUrl: online web page url 171 | //@return: the local file path of "index.html". If not exist, return nil 172 | 173 | - (NSURL *_Nullable)getFileURL:(NSURL *)webUrl; 174 | ``` 175 | ## Author 176 | [HUOLALA mobile technology team](https://juejin.cn/user/1768489241815070). 177 | ## License 178 | HLLOfflineWebVC is released under the Apache 2.0 license. See [LICENSE](LICENSE) for details. 179 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![license](https://img.shields.io/hexpm/l/plug.svg)](https://www.apache.org/licenses/LICENSE-2.0) 4 | ![Pod Version](https://img.shields.io/badge/pod-v1.0.0-green.svg) 5 | ![Pod Version](https://img.shields.io/badge/pod-v1.0.0-green.svg) 6 | ![Platform](https://img.shields.io/badge/platform-iOS-blue.svg) 7 | ![Language](https://img.shields.io/badge/language-ObjectC-green.svg) 8 | [![wiki](https://img.shields.io/badge/Wiki-open-brightgreen.svg)](https://juejin.cn/post/7103348563479887885) 9 | 10 | --- 11 | ## 介绍 12 | 13 |   HLLOfflineWebVC是货拉拉自研的轻量级高性能H5离线包sdk,可以显著的提升H5页面加载速度,iOS端基于[WKWebView](https://developer.apple.com/documentation/webkit/wkwebview/)实现。 14 | 主要原理为:提前缓存html、js、css、图片等资源文件到静态到本地,当H5页面请求资源时,尽量从本地获取数据,减少网络请求。更新原理细节参考文章[《货拉拉H5离线包原理与实践》](https://juejin.cn/post/7103348563479887885)。 15 | ## 比较 16 | | |未使用离线包 | 使用离线包 17 | |--|:-------------------------:|:-------------------------: 18 | | 耗时|2s | 1s 19 | |视频| | 20 | 21 | 22 | ## 特点 23 | - 安全可靠:无hook,无私有API,具有三重降级策略,保证可靠性 24 | - 容易维护:三层架构模式和模块化设计 25 | - 功能完备:功能可配置,数据埋点,开发者工具等功能一应俱全 26 | ## 依赖 27 | - iOS 9.0 或更高版本 28 | - Xcode 11.0 或更高版本 29 | - CocoaPods 1.11.2 或更高版本 30 | ## 指引 31 | 1) 下载源码到本地。 32 | 33 | 2) 进入”Example“工程目录,输入命令“pod install”安装第三方依赖库。 34 | 35 | 3) 使用Xcode打开工程“HLLOfflineWebVC.xcworkspace”,然后直接编译运行。 36 | 37 | 开源代码中使用GCDWebServer在本地搭建了离线包依赖的查询和下载接口,故可以直接本地体验示例工程,界面截图如下: 38 | 39 | 40 | 41 | ## 问题交流 42 | - 如果你发现了bug或者有其他功能诉求,欢迎提issue。 43 | - 如果想贡献代码,可以直接发起MR。 44 | ## 主要模块 45 | - OfflineWebPackage 46 | 47 | 离线包管理模块,核心模块,包含离线包查询、下载、缓存管理、数据上报功能。 48 | - OfflineWebDevTool 49 | 50 | 开发者debug调试工具。方便开发和测试阶段查看和清除离线包。 51 | - OfflineWebBisNameMatch 52 | 53 | 通过配置实现离线包URL和离线包ID自动匹配。 54 | - OfflineWebUtils 55 | 56 | 内部模块使用的辅助功能工具类。 57 | 58 | ## 使用 59 |   如果要在实际项目中使用,需要采取如下步骤: 60 | ### 离线包服务搭建 61 | - 实现查询接口 62 | 63 | https://www.huolala.cn/queryOffline?clientType=iOS&clientVer=1.0.0&offlineZipVer=1.0.0&bisName=xxx 64 | 65 | 请求参数接口参数说明: 66 | 67 | | 参数名 | 参数含义 | 备注 | 68 | |------------------|-------------------------------|-------------------------------------| 69 | | os | 终端类型 | iOS,Android | 70 | | clientVersion | 客户端版本 | 例如:1.0.0 | 71 | | bisName | 业务名,每个页面的离线包独立 | 例如:act3-offline-package-test | 72 | | offlineZipVer | 本地离线包版本 | 自定义参数,0表示本地无 | 73 | 74 | 75 |   查询结果返回结果为json,参数说明: 76 | 77 | | 参数名 | 参数含义 | 备注 | 78 | |--------------|-------------|--------------------------------------------------------------------------------------------| 79 | | bisName | 业务名 | 例如:act3-offline-package-test | 80 | | result | 结果 | -1 禁用离线包 0 无更新 1 有新离线包 | 81 | | url | 离线包(zip压缩包)下载地址 | 没有时为空字符串 | 82 | | refreshMode | 刷新模式 | 0 下次刷新(默认) 1 马上强制刷新(极端情况下使用) | 83 | | version | 离线包版本 | 例如:25609-j56gfa | 84 | 85 | - 接口跨域处理 86 | 87 | H5离线包加载的路径为文件路径为,发起cgi请求时,origin为null,需要网关或者后端添加跨域支持。 88 | 89 | ### H5端改造 90 | - 使用相对路径。引用的本地js、css等文件路径需要改成相对路径。 91 | - cookie、localstorage等存储跨域支持。 92 | - 加入版本文件。离线包资源包大包时加入版本描述文件".offweb.json",格式为: 93 | { "bisName": "xxx", "ver": "xxx" } 94 | 95 | ## 客户端接入 96 | 97 | ### 完整接入 98 |   安装离线包所有功能模块,包括自定义的Webview容器全部接入当前工程。 99 | 1) 安装离线包SDK 100 | 101 | 通过CocoaPods命令安装:pod 'HLLOfflineWebVC' 102 | 2) 离线包初始化 103 | 104 | 参考代码如下: 105 | ``` 106 | - (void)initOfflineWeb { 107 | NSDictionary *offwebConfigDict = DefaultWebPackageConfig; 108 | [HLLOfflineWebConfig setInitalParam:offwebConfigDict 109 | logBlock:^(HLLOfflineWebLogLevel level, NSString *keyword, NSString *message) { 110 | NSLog (@"use your log SDK :%d:%@:%@", (int) level, keyword, message); 111 | } 112 | reportBlock:^(NSString *event, NSDictionary *dict) { 113 | NSLog(@"data report:%@,%@", event, dict); 114 | } 115 | monitorBlock:^(HLLOfflineWebMonitorType type, NSString *key, CGFloat value, NSDictionary *lables) { 116 | NSLog(@"use your monitor sdk "); 117 | 118 | } 119 | env:@"prd" 120 | appversion:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]; 121 | } 122 | ``` 123 | 124 |   logBlock,reportBlock,monitorBlock 为具体的日志、埋点、实时监控SDK实现。 125 | 126 | 3) 通过参数字典配置功能 127 | 128 | 参数offwebConfigDict 为json转化成的字典,用于配置离线包的降级、自动拼接离线包参数配置等功能: 129 | ``` 130 | { 131 | "switch": 1, 132 | "predownloadlist": ["uappweb-offline"], 133 | "disablelist": [], 134 | "rules": [{ 135 | "host": ["test1.xxx.com", "test2.xxx.com"], 136 | "path": ["/uapp"], 137 | "offweb": "uappweb" 138 | } 139 | ] 140 | } 141 | ``` 142 | | 参数名 | 参数含义 | 是否必填 | 143 | |--------------|-------------|--------------------------------------------------------------------------------------------| 144 | | switch | 1 开启离线包功能,0 关闭 | 必填 | 145 | | predownloadlist | 预下载离线包列表 | 选填 | 146 | | disablelist | 需要禁用离线包功能到页面 | 选填 | 147 | | rules | H5页面和离线包参数映射规则 | 选填 148 | 149 | 150 | 4) webview容器适配。 151 | 参考开源代码中的代码,Webview容器需实现如下接口 152 | ``` 153 | - (BOOL)webview:(WKWebView *)webview shouldStartLoadWithRequest:(NSURLRequest *)request { 154 | return YES; 155 | } 156 | 157 | ``` 158 |   然后将HLLOfflineWebVC的父类修改为业务中的具体webview容器,同时头文件中声明WKWebview公共接口 159 | 160 | ### 仅安装核心模块 161 |    只安装离线包核心模块,不包含Webview容器,Cocoapods命令:pod 'HLLOfflineWebVC/OfflineWebPackage' 162 | 主要接口如下: 163 | 164 | 1) 离线包更新检查接口 165 | ``` 166 | /// @param bisName 离线包业务名 167 | /// @param resultBlock 结果回调 168 | 169 | - (void)checkUpdate:(NSString *)bisName result:(HLLOfflineWebResultBlock)resultBlock; 170 | ``` 171 | 2) 获取当前url对应的本地离线包中的index.html路径 172 | ``` 173 | /// @param webUrl 在线网页的url 174 | /// @return 对应的本地index文件路径,没有时返回nil 175 | - (NSURL *_Nullable)getFileURL:(NSURL *)webUrl; 176 | 177 | ``` 178 | ## 作者 179 |    [货拉拉移动端技术团队](https://juejin.cn/user/1768489241815070) 180 | ## 许可证 181 |   采用Apache2.0协议,详情参考[LICENSE](LICENSE) 182 | 183 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------