[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nA working minimal code example is preferred!\nIf you cannot produce example code please list the steps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\nDerivedData/\n.swiftpm/config/registries.json\n.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata\n.netrc\n"
  },
  {
    "path": "Examples/LazyPagerExample.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 60;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\tD309D3802E88B66E007A6FDC /* InsetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D309D37F2E88B66E007A6FDC /* InsetTest.swift */; };\n\t\tD33BC0522B69E6EE004B4338 /* SimpleExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33BC0512B69E6EE004B4338 /* SimpleExample.swift */; };\n\t\tD33F96FD2C62F582004D934A /* VerticalMediaPager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33F96FC2C62F582004D934A /* VerticalMediaPager.swift */; };\n\t\tD353FB592A52174B00C04ABE /* LazyPagerExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB582A52174B00C04ABE /* LazyPagerExampleApp.swift */; };\n\t\tD353FB5B2A52174B00C04ABE /* FullTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB5A2A52174B00C04ABE /* FullTestView.swift */; };\n\t\tD353FB5D2A52174C00C04ABE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D353FB5C2A52174C00C04ABE /* Assets.xcassets */; };\n\t\tD353FB602A52174C00C04ABE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D353FB5F2A52174C00C04ABE /* Preview Assets.xcassets */; };\n\t\tD353FB6A2A52174C00C04ABE /* LazyPagerExampleAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB692A52174C00C04ABE /* LazyPagerExampleAppTests.swift */; };\n\t\tD353FB742A52174C00C04ABE /* ImageScrollViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB732A52174C00C04ABE /* ImageScrollViewUITests.swift */; };\n\t\tD353FB762A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB752A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift */; };\n\t\tD367DA132A59E930004497D4 /* LazyPager in Frameworks */ = {isa = PBXBuildFile; productRef = D367DA122A59E930004497D4 /* LazyPager */; };\n\t\tD3776B5F2CF5658500AFB89D /* AnimatedPagerControlsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3776B5E2CF5658500AFB89D /* AnimatedPagerControlsExample.swift */; };\n\t\tD38D65E32C62E4B900AA140E /* LazyPager in Frameworks */ = {isa = PBXBuildFile; productRef = D38D65E22C62E4B900AA140E /* LazyPager */; };\n\t\tD3B3AEAC2DBD500800AC1E33 /* EnvironmentExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B3AEAB2DBD500500AC1E33 /* EnvironmentExample.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\tD353FB662A52174C00C04ABE /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = D353FB4D2A52174B00C04ABE /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = D353FB542A52174B00C04ABE;\n\t\t\tremoteInfo = ImageScrollView;\n\t\t};\n\t\tD353FB702A52174C00C04ABE /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = D353FB4D2A52174B00C04ABE /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = D353FB542A52174B00C04ABE;\n\t\t\tremoteInfo = ImageScrollView;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXFileReference section */\n\t\tD309D37F2E88B66E007A6FDC /* InsetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetTest.swift; sourceTree = \"<group>\"; };\n\t\tD33BC0512B69E6EE004B4338 /* SimpleExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleExample.swift; sourceTree = \"<group>\"; };\n\t\tD33F96FC2C62F582004D934A /* VerticalMediaPager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalMediaPager.swift; sourceTree = \"<group>\"; };\n\t\tD353FB552A52174B00C04ABE /* LazyPagerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LazyPagerExample.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD353FB582A52174B00C04ABE /* LazyPagerExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyPagerExampleApp.swift; sourceTree = \"<group>\"; };\n\t\tD353FB5A2A52174B00C04ABE /* FullTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullTestView.swift; sourceTree = \"<group>\"; };\n\t\tD353FB5C2A52174C00C04ABE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\tD353FB5F2A52174C00C04ABE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = \"Preview Assets.xcassets\"; sourceTree = \"<group>\"; };\n\t\tD353FB652A52174C00C04ABE /* LazyPagerExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LazyPagerExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD353FB692A52174C00C04ABE /* LazyPagerExampleAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyPagerExampleAppTests.swift; sourceTree = \"<group>\"; };\n\t\tD353FB6F2A52174C00C04ABE /* LazyPagerExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LazyPagerExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tD353FB732A52174C00C04ABE /* ImageScrollViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScrollViewUITests.swift; sourceTree = \"<group>\"; };\n\t\tD353FB752A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScrollViewUITestsLaunchTests.swift; sourceTree = \"<group>\"; };\n\t\tD3776B5E2CF5658500AFB89D /* AnimatedPagerControlsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedPagerControlsExample.swift; sourceTree = \"<group>\"; };\n\t\tD38D65E02C62E47C00AA140E /* LazyPager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = LazyPager; path = ..; sourceTree = \"<group>\"; };\n\t\tD3B3AEAB2DBD500500AC1E33 /* EnvironmentExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentExample.swift; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\tD353FB522A52174B00C04ABE /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD38D65E32C62E4B900AA140E /* LazyPager in Frameworks */,\n\t\t\t\tD367DA132A59E930004497D4 /* LazyPager in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD353FB622A52174C00C04ABE /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD353FB6C2A52174C00C04ABE /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\tD353FB4C2A52174B00C04ABE = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD353FB572A52174B00C04ABE /* LazyPagerExampleApp */,\n\t\t\t\tD353FB682A52174C00C04ABE /* LazyPagerExampleAppTests */,\n\t\t\t\tD353FB722A52174C00C04ABE /* LazyPagerExampleAppUITests */,\n\t\t\t\tD353FB562A52174B00C04ABE /* Products */,\n\t\t\t\tD367DA112A59E930004497D4 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD353FB562A52174B00C04ABE /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD353FB552A52174B00C04ABE /* LazyPagerExample.app */,\n\t\t\t\tD353FB652A52174C00C04ABE /* LazyPagerExampleTests.xctest */,\n\t\t\t\tD353FB6F2A52174C00C04ABE /* LazyPagerExampleUITests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD353FB572A52174B00C04ABE /* LazyPagerExampleApp */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD3B3AEAB2DBD500500AC1E33 /* EnvironmentExample.swift */,\n\t\t\t\tD353FB582A52174B00C04ABE /* LazyPagerExampleApp.swift */,\n\t\t\t\tD33F96FC2C62F582004D934A /* VerticalMediaPager.swift */,\n\t\t\t\tD33BC0512B69E6EE004B4338 /* SimpleExample.swift */,\n\t\t\t\tD309D37F2E88B66E007A6FDC /* InsetTest.swift */,\n\t\t\t\tD3776B5E2CF5658500AFB89D /* AnimatedPagerControlsExample.swift */,\n\t\t\t\tD353FB5A2A52174B00C04ABE /* FullTestView.swift */,\n\t\t\t\tD353FB5C2A52174C00C04ABE /* Assets.xcassets */,\n\t\t\t\tD353FB5E2A52174C00C04ABE /* Preview Content */,\n\t\t\t);\n\t\t\tpath = LazyPagerExampleApp;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD353FB5E2A52174C00C04ABE /* Preview Content */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD353FB5F2A52174C00C04ABE /* Preview Assets.xcassets */,\n\t\t\t);\n\t\t\tpath = \"Preview Content\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD353FB682A52174C00C04ABE /* LazyPagerExampleAppTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD353FB692A52174C00C04ABE /* LazyPagerExampleAppTests.swift */,\n\t\t\t);\n\t\t\tpath = LazyPagerExampleAppTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD353FB722A52174C00C04ABE /* LazyPagerExampleAppUITests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD353FB732A52174C00C04ABE /* ImageScrollViewUITests.swift */,\n\t\t\t\tD353FB752A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift */,\n\t\t\t);\n\t\t\tpath = LazyPagerExampleAppUITests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD367DA112A59E930004497D4 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tD38D65E02C62E47C00AA140E /* LazyPager */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\tD353FB542A52174B00C04ABE /* LazyPagerExample */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D353FB792A52174C00C04ABE /* Build configuration list for PBXNativeTarget \"LazyPagerExample\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD353FB512A52174B00C04ABE /* Sources */,\n\t\t\t\tD353FB522A52174B00C04ABE /* Frameworks */,\n\t\t\t\tD353FB532A52174B00C04ABE /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = LazyPagerExample;\n\t\t\tpackageProductDependencies = (\n\t\t\t\tD367DA122A59E930004497D4 /* LazyPager */,\n\t\t\t\tD38D65E22C62E4B900AA140E /* LazyPager */,\n\t\t\t);\n\t\t\tproductName = ImageScrollView;\n\t\t\tproductReference = D353FB552A52174B00C04ABE /* LazyPagerExample.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\tD353FB642A52174C00C04ABE /* LazyPagerExampleTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D353FB7C2A52174C00C04ABE /* Build configuration list for PBXNativeTarget \"LazyPagerExampleTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD353FB612A52174C00C04ABE /* Sources */,\n\t\t\t\tD353FB622A52174C00C04ABE /* Frameworks */,\n\t\t\t\tD353FB632A52174C00C04ABE /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\tD353FB672A52174C00C04ABE /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = LazyPagerExampleTests;\n\t\t\tproductName = ImageScrollViewTests;\n\t\t\tproductReference = D353FB652A52174C00C04ABE /* LazyPagerExampleTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\tD353FB6E2A52174C00C04ABE /* LazyPagerExampleUITests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = D353FB7F2A52174C00C04ABE /* Build configuration list for PBXNativeTarget \"LazyPagerExampleUITests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tD353FB6B2A52174C00C04ABE /* Sources */,\n\t\t\t\tD353FB6C2A52174C00C04ABE /* Frameworks */,\n\t\t\t\tD353FB6D2A52174C00C04ABE /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\tD353FB712A52174C00C04ABE /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = LazyPagerExampleUITests;\n\t\t\tproductName = ImageScrollViewUITests;\n\t\t\tproductReference = D353FB6F2A52174C00C04ABE /* LazyPagerExampleUITests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.ui-testing\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\tD353FB4D2A52174B00C04ABE /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = 1;\n\t\t\t\tLastSwiftUpdateCheck = 1420;\n\t\t\t\tLastUpgradeCheck = 1420;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\tD353FB542A52174B00C04ABE = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.2;\n\t\t\t\t\t};\n\t\t\t\t\tD353FB642A52174C00C04ABE = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.2;\n\t\t\t\t\t\tTestTargetID = D353FB542A52174B00C04ABE;\n\t\t\t\t\t};\n\t\t\t\t\tD353FB6E2A52174C00C04ABE = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.2;\n\t\t\t\t\t\tTestTargetID = D353FB542A52174B00C04ABE;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = D353FB502A52174B00C04ABE /* Build configuration list for PBXProject \"LazyPagerExample\" */;\n\t\t\tcompatibilityVersion = \"Xcode 14.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = D353FB4C2A52174B00C04ABE;\n\t\t\tpackageReferences = (\n\t\t\t\tD38D65E12C62E4B900AA140E /* XCLocalSwiftPackageReference \"../\" */,\n\t\t\t);\n\t\t\tproductRefGroup = D353FB562A52174B00C04ABE /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\tD353FB542A52174B00C04ABE /* LazyPagerExample */,\n\t\t\t\tD353FB642A52174C00C04ABE /* LazyPagerExampleTests */,\n\t\t\t\tD353FB6E2A52174C00C04ABE /* LazyPagerExampleUITests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\tD353FB532A52174B00C04ABE /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD353FB602A52174C00C04ABE /* Preview Assets.xcassets in Resources */,\n\t\t\t\tD353FB5D2A52174C00C04ABE /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD353FB632A52174C00C04ABE /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD353FB6D2A52174C00C04ABE /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\tD353FB512A52174B00C04ABE /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD353FB5B2A52174B00C04ABE /* FullTestView.swift in Sources */,\n\t\t\t\tD33F96FD2C62F582004D934A /* VerticalMediaPager.swift in Sources */,\n\t\t\t\tD3776B5F2CF5658500AFB89D /* AnimatedPagerControlsExample.swift in Sources */,\n\t\t\t\tD33BC0522B69E6EE004B4338 /* SimpleExample.swift in Sources */,\n\t\t\t\tD3B3AEAC2DBD500800AC1E33 /* EnvironmentExample.swift in Sources */,\n\t\t\t\tD353FB592A52174B00C04ABE /* LazyPagerExampleApp.swift in Sources */,\n\t\t\t\tD309D3802E88B66E007A6FDC /* InsetTest.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD353FB612A52174C00C04ABE /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD353FB6A2A52174C00C04ABE /* LazyPagerExampleAppTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tD353FB6B2A52174C00C04ABE /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tD353FB762A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift in Sources */,\n\t\t\t\tD353FB742A52174C00C04ABE /* ImageScrollViewUITests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\tD353FB672A52174C00C04ABE /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = D353FB542A52174B00C04ABE /* LazyPagerExample */;\n\t\t\ttargetProxy = D353FB662A52174C00C04ABE /* PBXContainerItemProxy */;\n\t\t};\n\t\tD353FB712A52174C00C04ABE /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = D353FB542A52174B00C04ABE /* LazyPagerExample */;\n\t\t\ttargetProxy = D353FB702A52174C00C04ABE /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin XCBuildConfiguration section */\n\t\tD353FB772A52174C00C04ABE /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.2;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD353FB782A52174C00C04ABE /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.2;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD353FB7A2A52174C00C04ABE /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"LazyPagerExampleApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = BX46265734;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.6;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollView;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD353FB7B2A52174C00C04ABE /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"LazyPagerExampleApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = BX46265734;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.6;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollView;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD353FB7D2A52174C00C04ABE /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = BX46265734;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.2;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/LazyPagerExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LazyPagerExample\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD353FB7E2A52174C00C04ABE /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = BX46265734;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 16.2;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/LazyPagerExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LazyPagerExample\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tD353FB802A52174C00C04ABE /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = BX46265734;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewUITests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_TARGET_NAME = ImageScrollView;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tD353FB812A52174C00C04ABE /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = BX46265734;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewUITests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_TARGET_NAME = ImageScrollView;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\tD353FB502A52174B00C04ABE /* Build configuration list for PBXProject \"LazyPagerExample\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD353FB772A52174C00C04ABE /* Debug */,\n\t\t\t\tD353FB782A52174C00C04ABE /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD353FB792A52174C00C04ABE /* Build configuration list for PBXNativeTarget \"LazyPagerExample\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD353FB7A2A52174C00C04ABE /* Debug */,\n\t\t\t\tD353FB7B2A52174C00C04ABE /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD353FB7C2A52174C00C04ABE /* Build configuration list for PBXNativeTarget \"LazyPagerExampleTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD353FB7D2A52174C00C04ABE /* Debug */,\n\t\t\t\tD353FB7E2A52174C00C04ABE /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tD353FB7F2A52174C00C04ABE /* Build configuration list for PBXNativeTarget \"LazyPagerExampleUITests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tD353FB802A52174C00C04ABE /* Debug */,\n\t\t\t\tD353FB812A52174C00C04ABE /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\n/* Begin XCLocalSwiftPackageReference section */\n\t\tD38D65E12C62E4B900AA140E /* XCLocalSwiftPackageReference \"../\" */ = {\n\t\t\tisa = XCLocalSwiftPackageReference;\n\t\t\trelativePath = ../;\n\t\t};\n/* End XCLocalSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */\n\t\tD367DA122A59E930004497D4 /* LazyPager */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = LazyPager;\n\t\t};\n\t\tD38D65E22C62E4B900AA140E /* LazyPager */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tproductName = LazyPager;\n\t\t};\n/* End XCSwiftPackageProductDependency section */\n\t};\n\trootObject = D353FB4D2A52174B00C04ABE /* Project object */;\n}\n"
  },
  {
    "path": "Examples/LazyPagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:/Users/brian/dev/LazyPager/Examples/LazyPagerExample.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "Examples/LazyPagerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/AnimatedPagerControlsExample.swift",
    "content": "import SwiftUI\nimport LazyPager\n\n\nstruct AnimatedPagerControlsExample: View {\n    \n    @State var data = [\n        \"nora1\",\n        \"nora2\",\n        \"nora3\",\n        \"nora4\",\n        \"nora5\",\n        \"nora6\",\n    ]\n    \n    @State var show = false\n    @State var index = 0\n    \n    var body: some View {\n        VStack {\n            LazyPager(data: data, page: $index) { element in\n                Image(element)\n                    .resizable()\n                    .aspectRatio(contentMode: .fit)\n            }\n            HStack(spacing: 20) {\n                Button(\"First\") {\n                    withAnimation {\n                        index = 0\n                    }\n                }\n                Button(\"Prev\") {\n                    withAnimation {\n                        if index > 0 {\n                            index -= 1\n                        }\n                    }\n                    \n                }\n                Button(\"Next\") {\n                    withAnimation {\n                        if index < data.count {\n                            index += 1\n                        }\n                    }\n                    \n                }\n                Button(\"Last\") {\n                    withAnimation {\n                        index = data.count - 1\n                    }\n                }\n            }\n        }\n    }\n}\n\nstruct AnimatedPagerControlsExample_Previews: PreviewProvider {\n    static var previews: some View {\n        AnimatedPagerControlsExample()\n    }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora1.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"356181627_737281678149026_5519646735590788375_n.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora2.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"356184881_810974757010572_166165563303848404_n.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora3.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"356184996_1504506290292039_6439519590743317419_n.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora4.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"356187567_797832131883666_8693445044613773171_n.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora5.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"356198313_803291668248047_1588179413198578920_n.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora6.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"358743821_933760767702238_5920729387861732707_n.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/EnvironmentExample.swift",
    "content": "//\n//  EnvironmentExample.swift\n//  LazyPagerExample\n//\n//  Created by Brian Floersch on 4/26/25.\n//\n\nimport SwiftUI\nimport LazyPager\n\nstruct SubView: View {\n    \n    @EnvironmentObject var textHolder: TextHolder\n    @Environment(\\.customValue) var customValue\n    \n    var parentText: String\n    var body: some View {\n        VStack {\n            Text(\"\\(textHolder.str) \\(parentText)\")\n                .font(.title)\n                .padding()\n            Text(\"Environment value: \\(customValue)\")\n                .font(.subheadline)\n                .padding()\n        }\n    }\n}\n\nstruct EnvironmentExample: View {\n    \n    @State var data = [\n        \"nora1\",\n        \"nora2\",\n        \"nora3\",\n        \"nora4\",\n        \"nora5\",\n        \"nora6\",\n    ]\n    \n    @State var show = false\n\n    var body: some View {\n        ZStack {\n            LazyPager(data: data) { element in\n                SubView(parentText: element)\n            }\n        }\n    }\n}\n\nclass TextHolder: ObservableObject {\n    let str: String\n    \n    init(str: String) {\n        self.str = str\n    }\n}\n\nprivate struct CustomEnvironmentKey: EnvironmentKey {\n    static let defaultValue: String = \"default value\"\n}\n\nextension EnvironmentValues {\n    var customValue: String {\n        get { self[CustomEnvironmentKey.self] }\n        set { self[CustomEnvironmentKey.self] = newValue }\n    }\n}\n\n#Preview {\n    EnvironmentExample()\n        .environmentObject(TextHolder(str: \"hello world\"))\n        .environment(\\.customValue, \"custom environment value\")\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/FullTestView.swift",
    "content": "//\n//  ContentView.swift\n//  LazyPager\n//\n//  Created by Brian Floersch on 7/2/23.\n//\n\nimport SwiftUI\nimport LazyPager\n\n\nstruct Foo {\n    let id = UUID()\n    var img: String\n    let idx: Int\n}\n\nstruct FullTestView: View {\n    \n    var direction: Direction\n    @State var data = [\n        Foo(img: \"nora1\", idx: 0),\n        Foo(img: \"nora2\", idx: 1),\n        Foo(img: \"nora3\", idx: 2),\n        Foo(img: \"nora4\", idx: 3),\n        Foo(img: \"nora5\", idx: 4),\n        Foo(img: \"nora6\", idx: 5),\n        Foo(img: \"nora1\", idx: 6),\n        Foo(img: \"nora2\", idx: 7),\n        Foo(img: \"nora3\", idx: 8),\n        Foo(img: \"nora4\", idx: 9),\n        Foo(img: \"nora5\", idx: 10),\n        Foo(img: \"nora6\", idx: 11),\n    ]\n    \n    @Binding var show: Bool\n    @State var opacity: CGFloat = 1\n    @State var index = 0\n    @State var loadPager = false\n    \n    var body: some View {\n        VStack {\n            LazyPager(data: data, page: $index, direction: direction) { element in\n                ZStack {\n                    Image(element.img)\n                        .resizable()\n                        .aspectRatio(contentMode: .fit)\n                    VStack {\n                        Text(\"\\(index) \\(element.idx) \\(data.count - 1)\")\n                            .foregroundColor(.black)\n                            .background(.white)\n                    }\n                }\n            }\n            .zoomable(min: 1, max: 5)\n            .onDismiss(backgroundOpacity: $opacity) {\n                show = false\n            }\n            .onTap {\n                print(\"tap\")\n            }\n            .onDoubleTap {\n                print(\"double tap\")\n            }\n            .shouldLoadMore(on: .lastElement(minus: 2)) {\n                data.append(Foo(img: \"nora4\", idx: data.count))\n            }\n            .overscroll { position in\n                if position == .beginning {\n                    print(\"Swiped past beginning\")\n                } else {\n                    print(\"Swiped past end\")\n                }\n            }\n            .onDrag {\n                print(\"Drag\")\n            }\n            .pageSpacing(10)\n            .background(.black.opacity(opacity))\n            .background(ClearFullScreenBackground())\n            .ignoresSafeArea()\n            VStack {\n                HStack(spacing: 30) {\n                    Button(\"-\") {\n                        index -= 1\n                    }\n                    VStack(spacing: 10) {\n                        Button(\"append\") {\n                            data.append(Foo(img: \"nora4\", idx: data.count + 1))\n                        }\n                        Button(\"replace\") {\n                            data[0] = Foo(img: \"nora4\", idx: data.count + 1)\n                        }\n                        Button(\"update\") {\n                            data[0].img = \"nora5\"\n                        }\n                    }\n                    VStack(spacing: 10) {\n                        Button(\"del first\") {\n                            data.remove(at: 0)\n                            index -= 1\n                        }\n                        Button(\"del last\") {\n                            data.remove(at: data.count - 1)\n                        }\n                        Button(\"jmp\") {\n                            index = 10\n                        }\n                    }\n                    Button(\"+\") {\n                        index += 1\n                    }\n                }\n                \n            }\n            .frame(maxWidth: .infinity)\n            .background(.white)\n        }\n    }\n}\n\nstruct FullTestView_Previews: PreviewProvider {\n    static var previews: some View {\n        FullTestView(direction: .horizontal, show: .constant(true))\n    }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/InsetTest.swift",
    "content": "import SwiftUI\nimport LazyPager\n\n\nstruct InsetTest: View {\n    \n    @State var data = [\n        \"nora1\",\n        \"nora2\",\n        \"nora3\",\n        \"nora4\",\n        \"nora5\",\n        \"nora6\",\n    ]\n    \n    @State var show = false\n    @State var ignoreSafeArea = true\n    \n    var body: some View {\n        NavigationStack {\n            ZStack {\n                \n                if ignoreSafeArea {\n                    LazyPager(data: data) { element in\n                        Image(element)\n                            .resizable()\n                            .aspectRatio(contentMode: .fit)\n                    }\n                    .ignoresSafeArea()\n                    \n                    VStack {\n                    }\n                    .frame(width: 300, height: 300)\n                    .background(.red)\n                    .frame(maxWidth: .infinity, maxHeight: .infinity)\n                    .ignoresSafeArea()\n                } else {\n                    LazyPager(data: data) { element in\n                        Image(element)\n                            .resizable()\n                            .aspectRatio(contentMode: .fit)\n                    }\n                    \n                    VStack {\n                    }\n                    .frame(width: 300, height: 300)\n                    .background(.red)\n                    .frame(maxWidth: .infinity, maxHeight: .infinity)\n                }\n                \n                VStack {\n                    Spacer()\n                    Toggle(\"ignore safe area\", isOn: $ignoreSafeArea)\n                        .padding()\n                }\n            }\n        }\n    }\n}\n\n#Preview {\n    InsetTest()\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/LazyPagerExampleApp.swift",
    "content": "//\n//  LazyPagerApp.swift\n//  LazyPager\n//\n//  Created by Brian Floersch on 7/2/23.\n//\n\nimport SwiftUI\n\n@main\nstruct LazyPagerApp: App {\n    @State var showFull = false\n    \n    var body: some Scene {\n        WindowGroup {\n            NavigationStack {\n                VStack(spacing: 20) {\n                    NavigationLink(destination: SimpleExample()) {\n                        Text(\"Simple Example\")\n                    }\n                    NavigationLink(destination: InsetTest()) {\n                        Text(\"Inset test\")\n                    }\n                    NavigationLink(destination: EnvironmentExample()\n                        .environmentObject(TextHolder(str: \"hello world\"))\n                        .environment(\\.customValue, \"custom environment value\")\n                    ) {\n                        Text(\"Environment Example\")\n                    }\n                    NavigationLink(destination: AnimatedPagerControlsExample()) {\n                        Text(\"Animated Pager Controls Example\")\n                    }\n                    Button(\"full Test View horizontal\") {\n                        showFull.toggle()\n                    }\n                    NavigationLink(destination: FullTestView(direction: .vertical, show: .constant(true))) {\n                        Text(\"Full Test View vertical\")\n                    }\n                    NavigationLink(destination: VerticalMediaPager()) {\n                        Text(\"Vertical media pager sample\")\n                    }\n                }\n            }\n            .fullScreenCover(isPresented: $showFull) {\n                FullTestView(direction: .horizontal, show: $showFull)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/SimpleExample.swift",
    "content": "import SwiftUI\nimport LazyPager\n\n\nstruct SimpleExample: View {\n    \n    @State var data = [\n        \"nora1\",\n        \"nora2\",\n        \"nora3\",\n        \"nora4\",\n        \"nora5\",\n        \"nora6\",\n    ]\n    \n    @State var show = false\n    \n    var body: some View {\n        LazyPager(data: data) { element in\n            Image(element)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n        }\n        .onDismiss {\n        }\n    }\n}\n\nstruct SimpleExample_Previews: PreviewProvider {\n    static var previews: some View {\n        SimpleExample()\n    }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleApp/VerticalMediaPager.swift",
    "content": "import SwiftUI\nimport LazyPager\n\n\nstruct VerticalMediaPager: View {\n    \n    @State var data = [\n        \"nora1\",\n        \"nora2\",\n        \"nora3\",\n        \"nora4\",\n        \"nora5\",\n        \"nora6\",\n    ]\n    @State var show = true\n    \n    var body: some View {\n        ZStack {\n            LazyPager(data: data, direction: .vertical) { element in\n                Image(element)\n                    .resizable()\n                    .aspectRatio(contentMode: .fill)\n            }\n            .overscroll {\n                if $0 == .beginning {\n                    print(\"Swiped past beginning\")\n                } else if $0 == .end {\n                    print(\"Swiped past end\")\n                }\n            }\n            .ignoresSafeArea()\n            \n            VStack(alignment: .leading) {\n                HStack {\n                    Spacer()\n                    VStack(spacing: 30) {\n                        Spacer()\n                        imgButton(\"heart.fill\")\n                        imgButton(\"text.bubble.fill\")\n                        imgButton(\"bookmark.fill\")\n                        imgButton(\"arrow.turn.up.right\")\n                    }\n                    .padding(.bottom, 20)\n                }\n                .padding()\n                Spacer()\n                HStack {\n                    VStack {\n                        Text(\"CatTok\")\n                            .frame(maxWidth: .infinity, alignment: .leading)\n                            .font(.title2)\n                        Text(\"Nora is an adorable cat\")\n                            .frame(maxWidth: .infinity, alignment: .leading)\n                    }\n                    Spacer()\n                    Text(\"😸\")\n                        .font(.title)\n                        .padding(5)\n                        .background(.pink.opacity(0.8))\n                        .clipShape(Circle())\n                }\n                .padding()\n                .foregroundColor(.white)\n                .background(.black.opacity(0.5))\n            }\n        }\n    }\n    \n    @ViewBuilder\n    func imgButton(_ name: String) -> some View {\n        Image(systemName: name)\n            .resizable()\n            .aspectRatio(contentMode: .fit)\n            .frame(width: 40, height: 40)\n            .foregroundColor(.white.opacity(0.9))\n    }\n}\n\n#Preview {\n    VerticalMediaPager()\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleAppTests/LazyPagerExampleAppTests.swift",
    "content": "//\n//  LazyPagerTests.swift\n//  LazyPagerTests\n//\n//  Created by Brian Floersch on 7/2/23.\n//\n\nimport XCTest\n@testable import LazyPager\n\nfinal class LazyPagerTests: XCTestCase {\n\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testExample() throws {\n        // This is an example of a functional test case.\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n        // Any test you write for XCTest can be annotated as throws and async.\n        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.\n        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.\n    }\n\n    func testPerformanceExample() throws {\n        // This is an example of a performance test case.\n        self.measure {\n            // Put the code you want to measure the time of here.\n        }\n    }\n\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleAppUITests/ImageScrollViewUITests.swift",
    "content": "//\n//  LazyPagerUITests.swift\n//  LazyPagerUITests\n//\n//  Created by Brian Floersch on 7/2/23.\n//\n\nimport XCTest\n\nfinal class LazyPagerUITests: XCTestCase {\n\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n\n        // In UI tests it is usually best to stop immediately when a failure occurs.\n        continueAfterFailure = false\n\n        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testExample() throws {\n        // UI tests must launch the application that they test.\n        let app = XCUIApplication()\n        app.launch()\n\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n    }\n\n    func testLaunchPerformance() throws {\n        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {\n            // This measures how long it takes to launch your application.\n            measure(metrics: [XCTApplicationLaunchMetric()]) {\n                XCUIApplication().launch()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Examples/LazyPagerExampleAppUITests/ImageScrollViewUITestsLaunchTests.swift",
    "content": "//\n//  LazyPagerUITestsLaunchTests.swift\n//  LazyPagerUITests\n//\n//  Created by Brian Floersch on 7/2/23.\n//\n\nimport XCTest\n\nfinal class LazyPagerUITestsLaunchTests: XCTestCase {\n\n    override class var runsForEachTargetApplicationUIConfiguration: Bool {\n        true\n    }\n\n    override func setUpWithError() throws {\n        continueAfterFailure = false\n    }\n\n    func testLaunch() throws {\n        let app = XCUIApplication()\n        app.launch()\n\n        // Insert steps here to perform after app launch but before taking a screenshot,\n        // such as logging into a test account or navigating somewhere in the app\n\n        let attachment = XCTAttachment(screenshot: app.screenshot())\n        attachment.name = \"Launch Screen\"\n        attachment.lifetime = .keepAlways\n        add(attachment)\n    }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Brian Floersch\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version: 5.7\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"LazyPager\",\n    platforms: [\n           .iOS(.v15)\n       ],\n    products: [\n        // Products define the executables and libraries a package produces, and make them visible to other packages.\n        .library(\n            name: \"LazyPager\",\n            targets: [\"LazyPager\"]),\n    ],\n    dependencies: [\n        // Dependencies declare other packages that this package depends on.\n        // .package(url: /* package url */, from: \"1.0.0\"),\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package. A target can define a module or a test suite.\n        // Targets can depend on other targets in this package, and on products in packages this package depends on.\n        .target(\n            name: \"LazyPager\",\n            dependencies: []),\n        .testTarget(\n            name: \"LazyPagerTests\",\n            dependencies: [\"LazyPager\"]),\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "# LazyPager for SwiftUI\n\nA buttery smooth, lazy loaded, panning, zooming, and gesture dismissible view pager view for SwiftUI. \n\nThe goal of this package is to expose a simple SwiftUI interface for a fluid and seamless content viewer. Unlike other pagers for SwiftUI - this is built on top of UIKit APIs exposing features not yet available in SwiftUI. \n\n### Horizontal\n<p align=\"center\">\n  <img src=\"https://github.com/gh123man/LazyPager/assets/959778/a82da8c3-9d65-4782-8fd7-40cc598e16da\" alt=\"animated\" />\n</p>\n\nThe above example is from [dateit](https://dateit.com/) demonstrating the capabilities of this library. Note: the overlay is custom and can be added by putting `LazyPager` inside a `ZStack`.\n\n### Vertical\n<p align=\"center\">\n  <img src=\"https://github.com/user-attachments/assets/21679506-c2ad-491c-8fe8-13fbd2b0aa2a\" alt=\"animated\" />\n</p>\n\nThe above example [can be found in the example project.](https://github.com/gh123man/SwiftUI-LazyPager/blob/master/Examples/LazyPagerExampleApp/VerticalMediaPager.swift)\n\n\n# Usage\n\n## Add the Swift Package\n\n1. Right click on your project -> `Add Package`\n2. In the search bar paste: `https://github.com/gh123man/LazyPager`\n3. Click `Add Package`\n\nOr add the package to your `Package.swift` if your project is a Swift package.\n\n\n## Examples\n\n### Simple Example\nA simple image pager that displays images by name from your app assets.\n\n```swift \n@State var data = [ ... ]\n\nvar body: some View {\n    LazyPager(data: data) { element in\n        Image(element)\n            .resizable()\n            .aspectRatio(contentMode: .fit)\n    }\n}\n```\n\nThat's it!\n\n### Detailed Example\n\n```swift \n@State var data = [ ... ]\n@State var show = true\n@State var opacity: CGFloat = 1 // Dismiss gesture background opacity \n@State var index = 0\n\nvar body: some View {\n    Button(\"Open\") {\n        show.toggle()\n    }\n    .fullScreenCover(isPresented: $show) {\n\n        // Provide any list of data and bind to an index\n        LazyPager(data: data, page: $index) { element in\n\n            // Supports any kind of view - not only images\n            Image(element)\n                .resizable()\n                .aspectRatio(contentMode: .fit)\n        }\n\n        // Make the content zoomable\n        .zoomable(min: 1, max: 5)\n\n        // Enable the swipe to dismiss gesture and background opacity control\n        .onDismiss(backgroundOpacity: $opacity) {\n            show = false\n        }\n\n        // Handle single tap gestures\n        .onTap {\n            print(\"tap\")\n        }\n\n        // Get notified when to load more content\n        .shouldLoadMore {\n            data.append(\"foobar\")\n        }\n        \n        // Get notified when swiping past the beginning or end of the list \n        .overscroll { position in\n            if position == .beginning {\n                print(\"Swiped past beginning\")\n            } else {\n                print(\"Swiped past end\")\n            }\n        }\n\n        // Handle double tap gestures\n        .onDoubleTap {\n            print(\"double tap\")\n        }\n        \n        // Handle drag events initiated by the user\n        .onDrag {\n            print(\"Drag\")\n        }\n\n        // Set the spacing between pages\n        .pageSpacing(20)\n\n        // Set the background color with the drag opacity control\n        .background(.black.opacity(opacity))\n\n        // A special included modifier to help make fullScreenCover transparent\n        .background(ClearFullScreenBackground())\n        \n        // Works with safe areas or ignored safe areas\n        .ignoresSafeArea()\n    }\n}\n```\n\n#### Vertical paging\n\n```swift \n@State var data = [ ... ]\n\nvar body: some View {\n    LazyPager(data: data, direction: .vertical) { element in\n        Image(element)\n            .resizable()\n            .aspectRatio(contentMode: .fill)\n    }\n}\n```\n\nFor a full working example, [open the sample project](https://github.com/gh123man/LazyPager/tree/master/Examples) in the examples folder, or [check out the code here](https://github.com/gh123man/SwiftUI-LazyPager/blob/master/Examples/LazyPagerExampleApp/FullTestView.swift)\n\n# Features\n\n- All content is lazy loaded. By default content is pre-loaded 3 elements ahead and behind the current index. \n- Display any kind of content - not just images! \n- Horizontal or Vertical paging.\n- Lazy loaded views are disposed when they are outside of the pre-load frame to conserve resources. \n- Enable zooming and panning with `.zoomable(min: CGFloat, max: CGFloat)`.\n- Double tap to zoom is also supported through `.zoomable` modifier.\n- Notifies when to load more content with `.shouldLoadMore`.\n- Notifies when you swipe past the beginning or end of data with `.overscroll`.\n- Animate page transitions by using `withAnimation` when changing the page index. \n- Works with `.ignoresSafeArea()` (or not) to get a true full screen view.\n- Drag to dismiss is supported with `.onDismiss` - Supply a binding opacity value to control the background opacity during the transition. \n- Tap events are handled internally, so use `.onTap` to handle single taps (useful for hiding and showing UI).\n- Use `.onDoubleTap` to get notified on double taps.\n- Use `.settings` to [modify advanced settings](https://github.com/gh123man/SwiftUI-LazyPager/blob/master/Sources/LazyPager/LazyPager.swift#L76).\n- Use `.absoluteContentPosition` to subscribe to content position updates (the index + the offset while paging)\n- Use `.onZoom` to get notified of the current zoom level\n- Use `.onDrag` to handle drag events when the user interacts with the view. No triggered when page is changed programmatically.\n- Use `.pageSpacing(CGFloat)` to set the spacing between pages.\n\n# Detailed usage\n\n## Working with `fullScreenCover`\n\n`fullScreenCover` is a good native element for displaying a photo browser, however it has an opaque background by default that is difficult to remove. So `LazyPager` provides a `ClearFullScreenBackground` background view you can use to fix it. Simply add `.background(ClearFullScreenBackground())` to the root element of your `fullScreenCover`. This makes the pull to dismiss gesture seamless. \n\n## Double tap to zoom\nYou can customize the double tap behavior using the `zoomable(min: CGFloat, max: CGFloat, doubleTapGesture: DoubleTap)`. By default `doubleTapGesture` is set to `.scale(0.5)` which means \"zoom 50% when double tapped\". You can change this to a different ratio or set it to `.disabled` to disable the double tap gesture. \n\n## Dismiss gesture handling \nBy default `.onDismiss` will be called after the pull to dismiss gesture is completed. It is often desirable to fade out the background in the process. `LazyPager` uses a fully transparent background by default so you can set your own custom background. NOTE: `.onDismiss` is only supported for `.horizontal` pagers.\n\nTo control the dismiss opacity of a custom background, use a `Binding<CGFloat>` like `.onDismiss(backgroundOpacity: $opacity) {` to fade out your custom background.\n"
  },
  {
    "path": "Sources/LazyPager/ClearFullScreenBackground.swift",
    "content": "//\n//  ClearFullScreenBackground.swift\n//  \n//\n//  Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\nimport SwiftUI\nimport UIKit\n\npublic struct ClearFullScreenBackground: UIViewRepresentable {\n    \n    public init() { }\n    \n    public func makeUIView(context: Context) -> UIView {\n        let view = UIView()\n        DispatchQueue.main.async {\n            view.superview?.superview?.backgroundColor = .clear\n        }\n        return view\n    }\n\n    public func updateUIView(_ uiView: UIView, context: Context) {}\n}\n"
  },
  {
    "path": "Sources/LazyPager/Collection+Extensions.swift",
    "content": "//\n//  Collection+Extensons.swift\n//  \n//\n//  Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\n\nextension Collection {\n    /// Returns the element at the specified index if it is within bounds, otherwise nil.\n    subscript (safe index: Index) -> Element? {\n        return indices.contains(index) ? self[index] : nil\n    }\n}\n\n"
  },
  {
    "path": "Sources/LazyPager/LazyPager.swift",
    "content": "//\n//  LazyPager.swift\n//  LazyPager\n//\n//  Created by Brian Floersch on 7/6/23.\n//\n\nimport Foundation\nimport UIKit\nimport SwiftUI\n\npublic enum LoadMore {\n    case lastElement(minus: Int = 0)\n}\n\npublic enum DoubleTap {\n    case disabled\n    case scale(CGFloat)\n}\n\npublic enum Direction {\n    case horizontal\n    case vertical\n}\n\npublic enum ListPosition {\n    case beginning\n    case end\n}\n\npublic enum ZoomConfig {\n    case disabled\n    case custom(min: CGFloat, max: CGFloat, doubleTap: DoubleTap)\n}\n\npublic struct Config<Element> {\n    /// binding variable to control a custom background opacity. LazyPager is transparent by default\n    public var backgroundOpacity: Binding<CGFloat>?\n    \n    /// Called when the view is done dismissing - dismiss gesture is disabled if nil\n    public var dismissCallback: (() -> ())?\n    \n    /// Called when tapping once\n    public var tapCallback: (() -> ())?\n    \n    /// Called when tapping twice\n    public var doubleTapCallback: (() -> ())?\n    \n    /// Called when dragging begins\n    public var dragCallback: (() -> ())?\n    \n    /// The offset used to trigger load loadMoreCallback\n    public var loadMoreOn: LoadMore = .lastElement(minus: 3)\n    \n    /// Called when more content should be loaded\n    public var loadMoreCallback: (() -> ())?\n    \n    /// Direction of the pager\n    public var direction : Direction = .horizontal\n    \n    /// Called whent the end of data is reached and the user tries to swipe again\n    public var overscrollCallback: ((ListPosition) -> ())?\n    \n    /// The element index + the offset while paging\n    public var absoluteContentPosition: Binding<CGFloat>?\n    \n    /// Called every view update to get the zoom config\n    public var zoomConfigGetter: (Element) -> ZoomConfig = { _ in .disabled }\n    \n    /// Called while zooming to provide the current zoom level for an element\n    public var onZoomHandler: ((Element, CGFloat) -> ())?\n\n    /// The spacing between pages. Defaults to 0.\n    public var pageSpacing: CGFloat = 0\n\n    /// Advanced settings (only accessible via .settings)\n    \n    /// How may out of view pages to load in advance (forward and backwards)\n    public var preloadAmount: Int = 3\n    \n    /// Minimum swipe velocity needed to trigger a dismiss\n    public var dismissVelocity: CGFloat = 1.3\n    \n    /// the minimum % (between 0 and 1) you need to drag to trigger a dismiss\n    public var dismissTriggerOffset: CGFloat = 0.1\n    \n    /// How long to animate the dismiss once done dragging\n    public var dismissAnimationLength: CGFloat = 0.2\n    \n    /// Cancel SwiftUI animations. Default to true because the dismiss gesture is already animated.\n    /// Stacking animations can cause undesirable behavior\n    public var shouldCancelSwiftUIAnimationsOnDismiss = true\n    \n    /// At what drag % (between 0 and 1) the background should be fully transparent\n    public var fullFadeOnDragAt: CGFloat = 0.2\n    \n    /// The minimum scroll distance the in which the pinch gesture is enabled\n    public var pinchGestureEnableOffset: Double = 10\n    \n    /// % ammount (from 0-1) of overscroll needed to call overscrollCallback\n    public var overscrollThreshold: Double = 0.15\n}\n\npublic struct LazyPager<Element, DataCollecton: RandomAccessCollection, Content: View> where DataCollecton.Index == Int, DataCollecton.Element == Element {\n    private var viewLoader: (Element) -> Content\n    private var data: DataCollecton\n    \n    @State private var defaultPageInternal = 0\n    private var providedPage: Binding<Int>?\n    \n    private var page: Binding<Int> {\n        providedPage ?? Binding(\n            get: { defaultPageInternal },\n            set: { defaultPageInternal = $0 }\n        )\n    }\n    \n    var config = Config<Element>()\n    \n    public init(data: DataCollecton,\n                page: Binding<Int>? = nil,\n                direction: Direction = .horizontal,\n                @ViewBuilder content: @escaping (Element) -> Content)  {\n        self.data = data\n        self.providedPage = page\n        self.viewLoader = content\n        self.config.direction = direction\n    }\n}\n\npublic extension LazyPager {\n    func onDismiss(backgroundOpacity: Binding<CGFloat>? = nil, _ callback: @escaping () -> ()) -> LazyPager {\n        guard config.direction == .horizontal else {\n            return self\n        }\n        var this = self\n        this.config.backgroundOpacity = backgroundOpacity\n        this.config.dismissCallback = callback\n        return this\n    }\n    \n    func onTap(_ callback: @escaping () -> ()) -> LazyPager {\n        var this = self\n        this.config.tapCallback = callback\n        return this\n    }\n    \n    func onDoubleTap(_ callback: @escaping () -> ()) -> LazyPager {\n        var this = self\n        this.config.doubleTapCallback = callback\n        return this\n    }\n    \n    func onDrag(_ callback: @escaping () -> ()) -> LazyPager {\n        var this = self\n        this.config.dragCallback = callback\n        return this\n    }\n    \n    func pageSpacing(_ spacing: CGFloat) -> LazyPager {\n        var this = self\n        this.config.pageSpacing = spacing\n        return this\n    }\n    \n    func shouldLoadMore(on: LoadMore = .lastElement(minus: 3), _ callback: @escaping () -> ()) -> LazyPager {\n        var this = self\n        this.config.loadMoreOn = on\n        this.config.loadMoreCallback = callback\n        return this\n    }\n    \n    func zoomable(min: CGFloat, max: CGFloat, doubleTapGesture: DoubleTap = .scale(0.5)) -> LazyPager {\n        var this = self\n        this.config.zoomConfigGetter = { _ in\n            return .custom(min: min, max: max, doubleTap: doubleTapGesture)\n        }\n        return this\n    }\n    \n    func zoomable(onElement: @escaping (Element) -> ZoomConfig) -> LazyPager {\n        var this = self\n        this.config.zoomConfigGetter = onElement\n        return this\n    }\n    \n    func settings(_ adjust: @escaping (inout Config<Element>) -> ()) -> LazyPager {\n        var this = self\n        adjust(&this.config)\n        return this\n    }\n    \n    func overscroll(_ callback: @escaping (ListPosition) -> ()) -> LazyPager {\n        var this = self\n        this.config.overscrollCallback = callback\n        return this\n    }\n    \n    func absoluteContentPosition(_ absoluteContentPosition: Binding<CGFloat>? = nil) -> LazyPager {\n        guard config.direction == .horizontal else {\n            return self\n        }\n        var this = self\n        this.config.absoluteContentPosition = absoluteContentPosition\n        return this\n    }\n    \n    func onZoom(_ onZoomHandler: @escaping (Element, CGFloat) -> ()) -> LazyPager {\n        var this = self\n        this.config.onZoomHandler = onZoomHandler\n        return this\n    }\n}\n\nextension LazyPager: UIViewControllerRepresentable {\n    public func makeUIViewController(context: Context) -> ViewDataProvider<Content, DataCollecton, Element> {\n        let provider = ViewDataProvider(data: data,\n                                        page: page,\n                                        config: config,\n                                        viewLoader: viewLoader)\n        DispatchQueue.main.async {\n            provider.goToPage(page.wrappedValue, animated: false)\n        }\n        return provider\n    }\n    \n    public func updateUIViewController(_ uiViewController: ViewDataProvider<Content, DataCollecton, Element>, context: Context) {\n        uiViewController.viewLoader = viewLoader\n        uiViewController.data = data\n        defer { uiViewController.reloadViews() }\n        if page.wrappedValue != uiViewController.pagerView.currentIndex {\n            // Index was explicitly updated\n            uiViewController.goToPage(page.wrappedValue, animated: context.transaction.animation != nil)\n        }\n        \n        if page.wrappedValue >= data.count {\n            uiViewController.goToPage(data.count - 1, animated: false)\n        } else if page.wrappedValue < 0 {\n            uiViewController.goToPage(0, animated: false)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/LazyPager/Math.swift",
    "content": "//\n//  Math.swift\n//  \n//\n//  Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\n\nfunc lerp(from: CGFloat, to: CGFloat, by: CGFloat) -> CGFloat {\n    return from * (1 - by) + to * by\n}\n\nfunc normalize(from min: CGFloat, at val: CGFloat, to max: CGFloat) -> CGFloat {\n    let v = (val - min) / (max - min)\n    return v < 0 ? 0 : v > 1 ? 1 : v\n}\n\n"
  },
  {
    "path": "Sources/LazyPager/PagerView.swift",
    "content": "//\n//  PagerView.swift\n//  \n//\n//  Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\nimport UIKit\nimport SwiftUI\n\nprotocol ViewLoader: AnyObject {\n    \n    associatedtype Element\n    associatedtype Content: View\n    \n    var dataCount: Int { get }\n    \n    func loadView(at: Int) -> ZoomableView<Element, Content>?\n    func updateHostedView(for zoomableView: ZoomableView<Element, Content>)\n}\n\nclass PagerView<Element, Loader: ViewLoader, Content: View>: UIScrollView, UIScrollViewDelegate where Loader.Element == Element, Loader.Content == Content {\n    \n    \n    var pageSpacing: CGFloat {\n        config.pageSpacing\n    }\n    var isFirstLoad = false\n    var loadedViews = [ZoomableView<Element, Content>]()\n    var config: Config<Element>\n    weak var viewLoader: Loader?\n    var lastBoundsSize: CGSize?\n    \n    var isRotating = false\n    var page: Binding<Int>\n    \n    var currentIndex: Int = 0 {\n        didSet {\n            loadMoreIfNeeded()\n        }\n    }\n    \n    var absoluteOffset: CGFloat {\n        var absoluteOffset: CGFloat\n        if config.direction == .horizontal {\n            absoluteOffset = self.contentOffset.x / self.pageWidth\n        } else {\n            absoluteOffset = self.contentOffset.y / self.pageHeight\n        }\n        return absoluteOffset\n    }\n    \n    var relativeIndex: Int {\n        if absoluteOffset.isInfinite || absoluteOffset.isNaN {\n            return 0\n        }\n        var idx = Int(round(absoluteOffset))\n        idx = idx < 0 ? 0 : idx\n        idx = idx >= loadedViews.count ? loadedViews.count-1 : idx\n        return idx\n    }\n    \n    var currentView: ZoomableView<Element, Content> {\n        loadedViews[relativeIndex]\n    }\n    \n    var pageWidth: CGFloat {\n        if config.direction == .horizontal {\n            return frame.width + pageSpacing\n        }\n        return frame.width\n    }\n    \n    var pageHeight: CGFloat {\n        if config.direction == .vertical {\n            return frame.height + pageSpacing\n        }\n        return frame.height\n    }\n    \n    init(page: Binding<Int>, config: Config<Element>) {\n        self.currentIndex = page.wrappedValue\n        self.page = page\n        self.config = config\n        super.init(frame: .zero)\n        \n        showsVerticalScrollIndicator = false\n        showsHorizontalScrollIndicator = false\n        backgroundColor = .clear\n        decelerationRate = .fast\n        delegate = self\n        contentInsetAdjustmentBehavior = .always\n        // DEBUG\n//        backgroundColor = .blue\n    }\n    \n    \n    required init?(coder: NSCoder) {\n        fatalError(\"Not implemented\")\n    }\n    \n    public override func layoutSubviews() {\n        super.layoutSubviews()\n        if !isFirstLoad {\n            ensureCurrentPage(animated: false)\n            isFirstLoad = true\n        } else if isRotating {\n            ensureCurrentPage(animated: false)\n        }\n    \n        // Ensures insets are updated when the screen rotates\n        if bounds.size != lastBoundsSize {\n            updateInsets(animated: lastBoundsSize != nil)\n            lastBoundsSize = bounds.size\n        }\n    }\n    \n    func computeViewState(immediate: Bool = false) {\n        delegate = nil\n        DispatchQueue.main.async {\n            self.delegate = self\n        }\n        \n        if subviews.isEmpty {\n            for i in currentIndex...(currentIndex + config.preloadAmount) {\n                if immediate {\n                    appendView(at: i)\n                } else {\n                    scheduleAppend(at: i)\n                }\n            }\n            for i in ((currentIndex - config.preloadAmount)..<currentIndex).reversed() {\n                if immediate {\n                    schedulePrepend(at: i)\n                } else {\n                    prependView(at: i)\n                }\n            }\n        }\n        \n        if let lastView = loadedViews.last {\n            let diff = lastView.index - currentIndex\n            if diff < (config.preloadAmount) {\n                for i in lastView.index..<(lastView.index + (config.preloadAmount - diff)) {\n                    if immediate {\n                        appendView(at: i + 1)\n                    } else {\n                        scheduleAppend(at: i + 1)\n                    }\n                }\n            }\n        }\n        \n        if let firstView = loadedViews.first {\n            let diff = currentIndex - firstView.index\n            if diff < (config.preloadAmount) {\n                for i in (firstView.index - (config.preloadAmount - diff)..<firstView.index).reversed() {\n                    if immediate {\n                        schedulePrepend(at: i)\n                    } else {\n                        prependView(at: i)\n                    }\n                }\n            }\n        }\n        self.removeOutOfFrameViews()\n        updateInsets(animated: false)\n        \n        // Debug\n//         print(self.loadedViews.map { $0.index })\n    }\n    \n    func updateInsets(animated: Bool) {\n        let update = {\n            self.contentInset = UIEdgeInsets(top: -self.safeAreaInsets.top,\n                                             left: -self.safeAreaInsets.left,\n                                             bottom: -self.safeAreaInsets.bottom,\n                                             right: -self.safeAreaInsets.right)\n        }\n        if !animated {\n            update()\n        } else {\n            UIView.animate(withDuration: 0.3) {\n                update()\n            }\n        }\n    }\n    \n    \n    func addSubview(_ zoomView: ZoomableView<Element, Content>) {\n        super.addSubview(zoomView)\n        NSLayoutConstraint.activate([\n            zoomView.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor),\n            zoomView.heightAnchor.constraint(equalTo: frameLayoutGuide.heightAnchor),\n        ])\n    }\n    \n    func addFirstView(_ zoomView: ZoomableView<Element, Content>) {\n        if config.direction == .horizontal {\n            zoomView.leadingConstraint = zoomView.leadingAnchor.constraint(equalTo: leadingAnchor)\n            zoomView.trailingConstraint = zoomView.trailingAnchor.constraint(equalTo: trailingAnchor)\n            zoomView.leadingConstraint?.isActive = true\n            zoomView.trailingConstraint?.isActive = true\n        } else {\n            zoomView.topConstraint = zoomView.topAnchor.constraint(equalTo: topAnchor)\n            zoomView.bottomConstraint = zoomView.bottomAnchor.constraint(equalTo: bottomAnchor)\n            zoomView.topConstraint?.isActive = true\n            zoomView.bottomConstraint?.isActive = true\n        }\n    }\n    \n    func scheduleAppend(at index: Int) {\n        DispatchQueue.main.async { [weak self] in\n            guard let self = self else { return }\n            // Ensure we are not trying to add a view that has already been loaded\n            if self.loadedViews.contains(where: { $0.index == index }) {\n                return\n            }\n            self.appendView(at: index)\n        }\n    }\n    \n    func appendView(at index: Int) {\n        guard let zoomView = viewLoader?.loadView(at: index) else { return }\n        \n        addSubview(zoomView)\n        \n        if let lastView = loadedViews.last {\n            if config.direction == .horizontal {\n                lastView.trailingConstraint?.isActive = false\n                lastView.trailingConstraint = nil\n                \n                zoomView.leadingConstraint = zoomView.leadingAnchor.constraint(equalTo: lastView.trailingAnchor, constant: pageSpacing)\n                zoomView.trailingConstraint = zoomView.trailingAnchor.constraint(equalTo: trailingAnchor)\n                zoomView.leadingConstraint?.isActive = true\n                zoomView.trailingConstraint?.isActive = true\n            } else {\n                lastView.bottomConstraint?.isActive = false\n                lastView.bottomConstraint = nil\n                \n                zoomView.topConstraint = zoomView.topAnchor.constraint(equalTo: lastView.bottomAnchor, constant: pageSpacing)\n                zoomView.bottomConstraint = zoomView.bottomAnchor.constraint(equalTo: bottomAnchor)\n                zoomView.topConstraint?.isActive = true\n                zoomView.bottomConstraint?.isActive = true\n            }\n            \n        } else {\n            addFirstView(zoomView)\n        }\n        loadedViews.append(zoomView)\n    }\n    \n    func schedulePrepend(at index: Int) {\n        DispatchQueue.main.async { [weak self] in\n            guard let self = self else { return }\n            // Ensure we are not trying to add a view that has already been loaded\n            if self.loadedViews.contains(where: { $0.index == index }) {\n                return\n            }\n            self.prependView(at: index)\n        }\n    }\n    \n    func prependView(at index: Int) {\n        guard let zoomView = viewLoader?.loadView(at: index) else { return }\n        \n        addSubview(zoomView)\n        \n        if let firstView = loadedViews.first {\n            if config.direction == .horizontal {\n                firstView.leadingConstraint?.isActive = false\n                firstView.leadingConstraint = nil\n                \n                zoomView.leadingConstraint = zoomView.leadingAnchor.constraint(equalTo: leadingAnchor)\n                zoomView.trailingConstraint = zoomView.trailingAnchor.constraint(equalTo: firstView.leadingAnchor, constant: -pageSpacing)\n                zoomView.leadingConstraint?.isActive = true\n                zoomView.trailingConstraint?.isActive = true\n            } else {\n                firstView.topConstraint?.isActive = false\n                firstView.topConstraint = nil\n                \n                zoomView.topConstraint = zoomView.topAnchor.constraint(equalTo: topAnchor)\n                zoomView.bottomConstraint = zoomView.bottomAnchor.constraint(equalTo: firstView.topAnchor, constant: -pageSpacing)\n                zoomView.topConstraint?.isActive = true\n                zoomView.bottomConstraint?.isActive = true\n            }\n            \n        } else {\n            addFirstView(zoomView)\n        }\n        \n        loadedViews.insert(zoomView, at: 0)\n        if config.direction == .horizontal {\n            contentOffset.x += pageWidth\n        } else {\n            contentOffset.y += pageHeight\n        }\n    }\n    \n    func reloadViews() {\n        for view in loadedViews {\n            viewLoader?.updateHostedView(for: view)\n        }\n    }\n    \n    func remove(view: ZoomableView<Element, Content>) {\n        guard let viewIndex = loadedViews.firstIndex(where: { $0.index == view.index }) else { return }\n        \n        let viewToDisconnect = loadedViews[viewIndex]\n        let prevView: ZoomableView<Element, Content>? = loadedViews[safe: viewIndex - 1]\n        let nextView: ZoomableView<Element, Content>? = loadedViews[safe: viewIndex + 1]\n        \n        let removedIndex = viewToDisconnect.index\n        \n        viewToDisconnect.removeFromSuperview()\n        loadedViews.remove(at: viewIndex)\n        \n        if config.direction == .horizontal {\n            if let prevView = prevView, let nextView = nextView {\n                // Both exist, removing from the middle\n                prevView.trailingConstraint?.isActive = false\n                prevView.trailingConstraint = prevView.trailingAnchor.constraint(equalTo: nextView.leadingAnchor, constant: -pageSpacing)\n                prevView.trailingConstraint?.isActive = true\n            } else if let prevView = prevView {\n                // This was the last view\n                prevView.trailingConstraint?.isActive = false\n                prevView.trailingConstraint = prevView.trailingAnchor.constraint(equalTo: trailingAnchor)\n                prevView.trailingConstraint?.isActive = true\n            } else if let nextView = nextView {\n                // This was the first view\n                nextView.leadingConstraint?.isActive = false\n                nextView.leadingConstraint = nextView.leadingAnchor.constraint(equalTo: leadingAnchor)\n                nextView.leadingConstraint?.isActive = true\n            }\n            \n            if removedIndex < (loadedViews.first?.index ?? 0) {\n                contentOffset.x -= pageWidth\n            }\n        } else {\n            if let prevView = prevView, let nextView = nextView {\n                // Both exist, removing from the middle\n                prevView.bottomConstraint?.isActive = false\n                prevView.bottomConstraint = prevView.bottomAnchor.constraint(equalTo: nextView.topAnchor, constant: -pageSpacing)\n                prevView.bottomConstraint?.isActive = true\n            } else if let prevView = prevView {\n                // This was the last view\n                prevView.bottomConstraint?.isActive = false\n                prevView.bottomConstraint = prevView.bottomAnchor.constraint(equalTo: bottomAnchor)\n                prevView.bottomConstraint?.isActive = true\n            } else if let nextView = nextView {\n                // This was the first view\n                nextView.topConstraint?.isActive = false\n                nextView.topConstraint = nextView.topAnchor.constraint(equalTo: topAnchor)\n                nextView.topConstraint?.isActive = true\n            }\n            \n            if removedIndex < (loadedViews.first?.index ?? 0) {\n                contentOffset.y -= pageHeight\n            }\n        }\n    }\n    \n    \n    func removeOutOfFrameViews() {\n        guard let viewLoader = viewLoader else { return }\n        \n        for view in loadedViews {\n            if abs(currentIndex - view.index) > config.preloadAmount || view.index >= viewLoader.dataCount {\n                remove(view: view)\n            }\n        }\n    }\n    \n    func resizeOutOfBoundsViews() {\n        for v in loadedViews {\n            if v.index != currentIndex {\n                v.zoomScale = 1\n            }\n        }\n    }\n    \n    func goToPage(_ page: Int, animated: Bool) {\n        currentIndex = page\n        DispatchQueue.main.async {\n            self.computeViewState(immediate: true)\n            self.ensureCurrentPage(animated: animated)\n        }\n    }\n    \n    func ensureCurrentPage(animated: Bool) {\n        guard let index = loadedViews.firstIndex(where: { $0.index == currentIndex }) else { return }\n        if config.direction == .horizontal {\n            setContentOffset(CGPoint(x: CGFloat(index) * pageWidth, y: contentOffset.y), animated: animated)\n        } else {\n            setContentOffset(CGPoint(x: contentOffset.x, y: CGFloat(index) * pageHeight), animated: animated)\n        }\n        self.currentView.dismissEnabled = true\n    }\n    \n    func loadMoreIfNeeded() {\n        guard let loadMoreCallback = config.loadMoreCallback else { return }\n        guard case let .lastElement(offset) = config.loadMoreOn else { return }\n        guard let viewLoader = viewLoader else { return }\n        \n        if currentIndex + offset >= viewLoader.dataCount - 1 {\n            DispatchQueue.main.async {\n                loadMoreCallback()\n            }\n        }\n    }\n    \n    func scrollingFinished() {\n        let newIndex = currentView.index\n        \n        if currentIndex != newIndex {\n            currentIndex = newIndex\n            page.wrappedValue = newIndex\n        }\n        \n        computeViewState()\n        \n        hasNotfiedOverscroll = false\n        resizeOutOfBoundsViews()\n        if loadedViews.isEmpty { return }\n        currentView.dismissEnabled = true\n    }\n    \n    // MARK: UISCrollVieDelegate methods\n    \n    var lastPos: CGFloat = 0\n    var hasNotfiedOverscroll = false\n    \n    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {\n        config.dragCallback?()\n    }\n\n    public func scrollViewDidScroll(_ scrollView: UIScrollView) {\n        \n        if !scrollView.isTracking, !isRotating, (currentView.index != page.wrappedValue || page.wrappedValue != currentIndex ) {\n            currentIndex = currentView.index\n            page.wrappedValue = currentIndex\n        }\n        \n        if let index = loadedViews[safe: relativeIndex]?.index {\n            config.absoluteContentPosition?.wrappedValue = CGFloat(index) + absoluteOffset - CGFloat(relativeIndex)\n        }\n        \n        if !hasNotfiedOverscroll {\n            if relativeIndex >= loadedViews.count-1, absoluteOffset - CGFloat(relativeIndex) > config.overscrollThreshold {\n                config.overscrollCallback?(.end)\n                hasNotfiedOverscroll = true\n            }\n            \n            if relativeIndex <= 0, absoluteOffset - CGFloat(relativeIndex) < -config.overscrollThreshold {\n                config.overscrollCallback?(.beginning)\n                hasNotfiedOverscroll = true\n            }\n        }\n        \n        if loadedViews.isEmpty { return }\n        self.currentView.dismissEnabled = false\n    }\n    \n    public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {\n        \n        let targetPage: CGFloat\n        let velocityThreshold: CGFloat = 0.5 // a value to tune\n        \n        if config.direction == .horizontal {\n            let currentRelativePage = scrollView.contentOffset.x / pageWidth\n            if velocity.x > velocityThreshold {\n                // Swiped forward\n                targetPage = floor(currentRelativePage + 1)\n            } else if velocity.x < -velocityThreshold {\n                // Swiped backward\n                targetPage = ceil(currentRelativePage - 1)\n            } else {\n                // No strong swipe, snap to nearest\n                targetPage = round(currentRelativePage)\n            }\n            targetContentOffset.pointee.x = targetPage * pageWidth\n        } else {\n            let currentRelativePage = scrollView.contentOffset.y / pageHeight\n            if velocity.y > velocityThreshold {\n                targetPage = floor(currentRelativePage + 1)\n            } else if velocity.y < -velocityThreshold {\n                targetPage = ceil(currentRelativePage - 1)\n            } else {\n                targetPage = round(currentRelativePage)\n            }\n            targetContentOffset.pointee.y = targetPage * pageHeight\n        }\n    }\n    \n    public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {\n        if !decelerate {\n            scrollingFinished()\n        }\n    }\n    \n    public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {\n        scrollingFinished()\n    }\n}\n"
  },
  {
    "path": "Sources/LazyPager/ViewDataProvider.swift",
    "content": "//\n//  ViewDataProvider.swift\n//  \n//\n//  Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\nimport SwiftUI\nimport UIKit\n\npublic class ViewDataProvider<Content: View, DataCollecton: RandomAccessCollection, Element>: UIViewController, ViewLoader where DataCollecton.Index == Int, DataCollecton.Element == Element {\n    \n    var viewLoader: (Element) -> Content\n    var data: DataCollecton\n    var config: Config<Element>\n    var pagerView: PagerView<Element, ViewDataProvider, Content>\n    \n    var dataCount: Int {\n        return data.count\n    }\n    \n    init(data: DataCollecton,\n         page: Binding<Int>,\n         config: Config<Element>,\n         viewLoader: @escaping (Element) -> Content) {\n        \n        self.data = data\n        self.viewLoader = viewLoader\n        self.config = config\n        self.pagerView = PagerView(page: page, config: config)\n\n        super.init(nibName: nil, bundle: nil)\n        self.pagerView.viewLoader = self\n        \n        pagerView.computeViewState()\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    func goToPage(_ page: Int, animated: Bool) {\n        pagerView.goToPage(page, animated: animated)\n    }\n    \n    func reloadViews() {\n        pagerView.reloadViews()\n        pagerView.computeViewState()\n    }\n\n    // MARK: ViewLoader\n    \n    func loadView(at index: Int) -> ZoomableView<Element, Content>? {\n        guard let dta = data[safe: index] else { return nil }\n        \n        let hostingController = UIHostingController(rootView: viewLoader(dta))\n        return ZoomableView(hostingController: hostingController, index: index, data: dta, config: config)\n    }\n    \n    func updateHostedView(for zoomableView: ZoomableView<Element, Content>) {\n        guard let dta = data[safe: zoomableView.index] else { return }\n        \n        zoomableView.hostingController.rootView = viewLoader(dta)\n    }\n    \n    // MARK: UIViewController\n    \n    public override func loadView() {\n        self.view = pagerView\n    }\n    \n    public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {\n        super.viewWillTransition(to: size, with: coordinator)\n        \n        pagerView.isRotating = true\n        self.pagerView.scrollingFinished()\n        coordinator.animate(alongsideTransition: { context in }, completion: { context in\n            self.pagerView.isRotating = false\n            DispatchQueue.main.async {\n                self.pagerView.goToPage(self.pagerView.currentIndex, animated: false)\n            }\n        })\n    }\n}\n\n"
  },
  {
    "path": "Sources/LazyPager/ZoomableView.swift",
    "content": "//\n//  ZoomableView.swift\n//  LazyPager\n//\n//  Created by Brian Floersch on 7/4/23.\n//\n\nimport Foundation\nimport UIKit\nimport SwiftUI\n\nclass ZoomableView<Element, Content: View>: UIScrollView, UIScrollViewDelegate, UIGestureRecognizerDelegate {\n    \n    var trailingConstraint: NSLayoutConstraint?\n    var leadingConstraint: NSLayoutConstraint?\n    var topConstraint: NSLayoutConstraint?\n    var bottomConstraint: NSLayoutConstraint?\n    var contentTopToContent: NSLayoutConstraint!\n    var contentTopToFrame: NSLayoutConstraint!\n    var contentBottomToFrame: NSLayoutConstraint!\n    var contentBottomToView: NSLayoutConstraint!\n    \n    var config: Config<Element>\n    var bottomView: UIView\n    \n    var allowScroll: Bool = true {\n        didSet {\n            if allowScroll, config.direction == .horizontal {\n                contentTopToFrame.isActive = false\n                contentBottomToFrame.isActive = false\n                bottomView.isHidden = false\n                \n                contentTopToContent.isActive = true\n                contentBottomToView.isActive = true\n            } else {\n                contentTopToContent.isActive = false\n                contentBottomToView.isActive = false\n                \n                contentTopToFrame.isActive = true\n                contentBottomToFrame.isActive = true\n                bottomView.isHidden = true\n            }\n        }\n    }\n    \n    var wasTracking = false\n    var isZoomHappening = false\n    var dismissEnabled = false // Contorlled by PagerView to prevent flicker\n    var lastInset: CGFloat = 0\n    var currentZoomInsetAnimation: UIViewPropertyAnimator?\n    \n    var hostingController: UIHostingController<Content>\n    var index: Int\n    var data: Element\n    var doubleTap: DoubleTap?\n    var lastBoundsSize: CGSize?\n    \n    var view: UIView {\n        return hostingController.view\n    }\n    \n    init(hostingController: UIHostingController<Content>, index: Int, data: Element, config: Config<Element>) {\n        self.index = index\n        self.hostingController = hostingController\n        self.data = data\n        self.config = config\n        \n        let v = UIView()\n        self.bottomView = v\n        \n        super.init(frame: .zero)\n        \n        translatesAutoresizingMaskIntoConstraints = false\n        delegate = self\n        panGestureRecognizer.delegate = self\n        \n        updateZoomConfig()\n        \n        bouncesZoom = true\n        backgroundColor = .clear\n        alwaysBounceVertical = false\n        contentInsetAdjustmentBehavior = .never\n        if config.dismissCallback != nil {\n            alwaysBounceVertical = true\n        }\n        showsVerticalScrollIndicator = false\n        showsHorizontalScrollIndicator = false\n        \n        view.translatesAutoresizingMaskIntoConstraints = false\n        view.backgroundColor = .clear\n        decelerationRate = .fast\n        // DEBUG\n//        backgroundColor = .red\n        addSubview(view)\n        \n        NSLayoutConstraint.activate([\n            view.leadingAnchor.constraint(equalTo: leadingAnchor),\n            view.trailingAnchor.constraint(equalTo: trailingAnchor),\n            view.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor),\n            view.heightAnchor.constraint(equalTo: frameLayoutGuide.heightAnchor),\n        ])\n        \n        contentTopToFrame = view.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor)\n        contentTopToContent = view.topAnchor.constraint(equalTo: topAnchor)\n        contentBottomToFrame = view.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor)\n        contentBottomToView = view.bottomAnchor.constraint(equalTo: bottomView.topAnchor)\n        \n        v.translatesAutoresizingMaskIntoConstraints = false\n        addSubview(v)\n\n//        This is for future support of a drawer view\n        let constant: CGFloat = config.dismissCallback == nil ? 0 : 1\n\n        NSLayoutConstraint.activate([\n          v.bottomAnchor.constraint(equalTo: bottomAnchor),\n          v.leadingAnchor.constraint(equalTo: frameLayoutGuide.leadingAnchor),\n          v.trailingAnchor.constraint(equalTo: frameLayoutGuide.trailingAnchor),\n          v.heightAnchor.constraint(equalToConstant: constant)\n        ])\n        \n        var singleTapGesture: UITapGestureRecognizer?\n        if config.tapCallback != nil {\n            let gesture = UITapGestureRecognizer(target: self, action: #selector(singleTap(_:)))\n            gesture.numberOfTapsRequired = 1\n            gesture.numberOfTouchesRequired = 1\n            addGestureRecognizer(gesture)\n            singleTapGesture = gesture\n        }\n                \n        func setupDoubleTapGesture() {\n            let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(onDoubleTap(_:)))\n            doubleTapRecognizer.numberOfTapsRequired = 2\n            doubleTapRecognizer.numberOfTouchesRequired = 1\n            addGestureRecognizer(doubleTapRecognizer)            \n            singleTapGesture?.require(toFail: doubleTapRecognizer)\n        }\n        \n        if case .scale = doubleTap {\n            setupDoubleTapGesture()\n        } else if config.doubleTapCallback != nil {\n            setupDoubleTapGesture()\n        }\n        \n        DispatchQueue.main.async {\n            self.updateState()\n        }\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"Not implemented\")\n    }\n    \n    func updateZoomConfig() {\n        switch config.zoomConfigGetter(data) {\n        case .disabled:\n            maximumZoomScale = 1\n            minimumZoomScale = 1\n            doubleTap = nil\n        case let .custom(min, max, doubleTap):\n            minimumZoomScale = min\n            maximumZoomScale = max\n            self.doubleTap = doubleTap\n        }\n    }\n    \n    @objc func singleTap(_ recognizer: UITapGestureRecognizer) {\n        config.tapCallback?()\n    }\n    \n    @objc func onDoubleTap(_ recognizer: UITapGestureRecognizer) {\n        config.doubleTapCallback?()\n        \n        if case let .scale(scale) = doubleTap {\n            let pointInView = recognizer.location(in: view)\n            zoom(at: pointInView, scale: scale)\n            updateInsets()\n        }\n    }\n    \n    func updateState() {\n        updateZoomConfig()\n        allowScroll = zoomScale == 1\n\n        if contentOffset.y > config.pinchGestureEnableOffset, allowScroll {\n            pinchGestureRecognizer?.isEnabled = false\n        } else {\n            pinchGestureRecognizer?.isEnabled = true\n        }\n        \n        if allowScroll {\n            if dismissEnabled, config.dismissCallback != nil {\n                let offset = contentOffset.y\n                if offset < 0 {\n                    let absoluteDragOffset = normalize(from: 0, at: abs(offset), to: frame.size.height)\n                    let fadeOffset = normalize(from: 0, at: absoluteDragOffset, to: config.fullFadeOnDragAt)\n                    config.backgroundOpacity?.wrappedValue = 1 - fadeOffset\n                } else {\n                    DispatchQueue.main.async {\n                        self.config.backgroundOpacity?.wrappedValue = 1\n                    }\n                }\n            }\n            wasTracking = isTracking\n        }\n    }\n    \n    func zoom(at point: CGPoint, scale: CGFloat) {\n        let mid = lerp(from: minimumZoomScale, to: maximumZoomScale, by: scale)\n        let newZoomScale = zoomScale == minimumZoomScale ? mid : minimumZoomScale\n        let size = bounds.size\n        let w = size.width / newZoomScale\n        let h = size.height / newZoomScale\n        let x = point.x - (w * 0.5)\n        let y = point.y - (h * 0.5)\n        zoom(to: CGRect(x: x, y: y, width: w, height: h), animated: true)\n    }\n    \n    override func layoutSubviews() {\n        super.layoutSubviews()\n        // Ensures insets are updated when the screen rotates\n        if bounds.size != lastBoundsSize {\n            lastBoundsSize = bounds.size\n            updateInsets()\n        }\n    }\n    \n    // MARK: UIScrollViewDelegate methods\n    \n    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {\n        isZoomHappening = true\n        updateState()\n    }\n    \n    func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {\n        isZoomHappening = false\n        updateState()\n        updateInsets()\n    }\n    \n    func scrollViewDidScroll(_ scrollView: UIScrollView) {\n        updateState()\n    }\n    \n    func viewForZooming(in scrollView: UIScrollView) -> UIView? {\n        return view\n    }\n    \n    func updateInsets() {\n        let w: CGFloat = view.intrinsicContentSize.width * UIScreen.main.scale\n        let h: CGFloat = view.intrinsicContentSize.height * UIScreen.main.scale\n        \n        let ratioW = view.frame.width / w\n        let ratioH = view.frame.height / h\n        \n        let ratio = ratioW < ratioH ? ratioW : ratioH\n        \n        let newWidth = w*ratio\n        let newHeight = h*ratio\n        \n        let left = 0.5 * (newWidth * zoomScale > view.frame.width\n                          ? (newWidth - view.frame.width)\n                          : (frame.width - view.frame.width))\n        let top = 0.5 * (newHeight * zoomScale > view.frame.height\n                         ? (newHeight - view.frame.height)\n                         : (frame.height - view.frame.height))\n        \n        if zoomScale <= maximumZoomScale {\n            let targetInsets = UIEdgeInsets(\n                top: top,\n                left: left,\n                bottom: top,\n                right: left\n            )\n            \n            UIView.animate(withDuration: 0.3) {\n                self.contentInset = targetInsets\n            }\n        }\n    }\n    \n    func scrollViewDidZoom(_ scrollView: UIScrollView) {\n\n        let scrollViewSize = scrollView.bounds.size\n        let zoomViewSize = view.frame.size\n\n        let horizontalInset = max(0, (scrollViewSize.width - zoomViewSize.width) / 2)\n        let verticalInset = max(0, (scrollViewSize.height - zoomViewSize.height) / 2)\n\n        scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)\n        config.onZoomHandler?(data, scrollView.zoomScale)\n    }\n    \n    \n    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {\n        \n        let percentage = contentOffset.y / (contentSize.height - bounds.size.height)\n        \n        if wasTracking,\n           percentage < -config.dismissTriggerOffset,\n           !isZoomHappening,\n           velocity.y < -config.dismissVelocity,\n           config.dismissCallback != nil {\n            \n            dismissEnabled = false // prevent touch interaction from messing with animation of opacity.\n            let ogFram = frame.origin\n            \n            withAnimation(.linear(duration: self.config.dismissAnimationLength)) {\n                self.config.backgroundOpacity?.wrappedValue = 0\n            }\n            \n            frame.origin.y = -contentOffset.y\n            \n            UIView.animate(withDuration: self.config.dismissAnimationLength, animations: {\n                self.frame.origin = CGPoint(x: ogFram.x, y: self.frame.size.height)\n            }) { _ in\n                if self.config.shouldCancelSwiftUIAnimationsOnDismiss {\n                    var transaction = Transaction()\n                    transaction.disablesAnimations = true\n                    withTransaction(transaction) {\n                        self.config.dismissCallback?()\n                    }\n                } else {\n                    self.config.dismissCallback?()\n                }\n            }\n        }\n    }\n    \n    // MARK: UIGestureRecognizerDelegate\n    \n    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {\n        // We only want to intercept our own pan gesture.\n        guard gestureRecognizer == self.panGestureRecognizer else {\n            return true\n        }\n\n        let panGesture = self.panGestureRecognizer\n        let velocity = panGesture.velocity(in: self)\n\n        // This logic is for the horizontal pager.\n        if config.direction == .horizontal {\n            // If the swipe is mostly vertical, it's for dismissal. Let it happen.\n            if abs(velocity.y) > abs(velocity.x) {\n                return true\n            }\n\n            // It's a horizontal swipe. Should we let our own pan gesture begin?\n\n            // If not zoomed, NO. The pager should handle all horizontal movement.\n            if zoomScale <= minimumZoomScale {\n                return false // Prevent our pan, let PagerView handle it.\n            }\n\n            // If we ARE zoomed, check if we're at the horizontal edges.\n            let maxOffsetX = contentSize.width - bounds.width + contentInset.right\n            let minOffsetX = -contentInset.left\n            \n            let isAtRightEdge = contentOffset.x >= maxOffsetX - 1.0\n            let isAtLeftEdge = contentOffset.x <= minOffsetX + 1.0\n\n            // At the right edge and trying to swipe left (to the next page).\n            if isAtRightEdge && velocity.x < 0 {\n                return false // Prevent our pan, let PagerView handle it.\n            }\n\n            // At the left edge and trying to swipe right (to the previous page).\n            if isAtLeftEdge && velocity.x > 0 {\n                return false // Prevent our pan, let PagerView handle it.\n            }\n        } else { // Vertical Pager\n            // If the swipe is mostly horizontal, let it happen.\n            if abs(velocity.x) > abs(velocity.y) {\n                return true\n            }\n\n            // It's a vertical swipe. Should we let our own pan gesture begin?\n\n            // If not zoomed, NO. The pager should handle all vertical movement.\n            if zoomScale <= minimumZoomScale {\n                return false // Prevent our pan, let PagerView handle it.\n            }\n\n            // If we ARE zoomed, check if we're at the vertical edges.\n            let maxOffsetY = contentSize.height - bounds.height + contentInset.bottom\n            let minOffsetY = -contentInset.top\n\n            let isAtBottomEdge = contentOffset.y >= maxOffsetY - 1.0\n            let isAtTopEdge = contentOffset.y <= minOffsetY + 1.0\n\n            // At the bottom edge and trying to swipe up (to the next page).\n            if isAtBottomEdge && velocity.y < 0 {\n                return false // Prevent our pan, let PagerView handle it.\n            }\n\n            // At the top edge and trying to swipe down (to the previous page).\n            if isAtTopEdge && velocity.y > 0 {\n                return false // Prevent our pan, let PagerView handle it.\n            }\n        }\n        \n        // If we're not at an edge while zoomed, our pan gesture should begin.\n        return true\n    }\n\n    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {\n        // We no longer need simultaneous recognition. This simplifies the logic and\n        // prevents the differential panning issues.\n        return false\n    }\n}\n"
  },
  {
    "path": "Tests/LazyPagerTests/LazyPagerTests.swift",
    "content": "import XCTest\n@testable import LazyPager\n\nfinal class LazyPagerTests: XCTestCase {\n    func testExample() throws {\n        // This is an example of a functional test case.\n        // Use XCTAssert and related functions to verify your tests produce the correct\n        // results.\n//        XCTAssertEqual(LazyPager().text, \"Hello, World!\")\n    }\n}\n"
  }
]