Repository: gh123man/SwiftUI-LazyPager
Branch: master
Commit: be4b1952c105
Files: 36
Total size: 99.8 KB
Directory structure:
gitextract_8knwvjye/
├── .github/
│ └── ISSUE_TEMPLATE/
│ └── bug_report.md
├── .gitignore
├── Examples/
│ ├── LazyPagerExample.xcodeproj/
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ ├── LazyPagerExampleApp/
│ │ ├── AnimatedPagerControlsExample.swift
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── nora1.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora2.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora3.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora4.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora5.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── nora6.imageset/
│ │ │ └── Contents.json
│ │ ├── EnvironmentExample.swift
│ │ ├── FullTestView.swift
│ │ ├── InsetTest.swift
│ │ ├── LazyPagerExampleApp.swift
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ ├── SimpleExample.swift
│ │ └── VerticalMediaPager.swift
│ ├── LazyPagerExampleAppTests/
│ │ └── LazyPagerExampleAppTests.swift
│ └── LazyPagerExampleAppUITests/
│ ├── ImageScrollViewUITests.swift
│ └── ImageScrollViewUITestsLaunchTests.swift
├── LICENSE
├── Package.swift
├── README.md
├── Sources/
│ └── LazyPager/
│ ├── ClearFullScreenBackground.swift
│ ├── Collection+Extensions.swift
│ ├── LazyPager.swift
│ ├── Math.swift
│ ├── PagerView.swift
│ ├── ViewDataProvider.swift
│ └── ZoomableView.swift
└── Tests/
└── LazyPagerTests/
└── LazyPagerTests.swift
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
A working minimal code example is preferred!
If you cannot produce example code please list the steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
================================================
FILE: Examples/LazyPagerExample.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
D309D3802E88B66E007A6FDC /* InsetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D309D37F2E88B66E007A6FDC /* InsetTest.swift */; };
D33BC0522B69E6EE004B4338 /* SimpleExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33BC0512B69E6EE004B4338 /* SimpleExample.swift */; };
D33F96FD2C62F582004D934A /* VerticalMediaPager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33F96FC2C62F582004D934A /* VerticalMediaPager.swift */; };
D353FB592A52174B00C04ABE /* LazyPagerExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB582A52174B00C04ABE /* LazyPagerExampleApp.swift */; };
D353FB5B2A52174B00C04ABE /* FullTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB5A2A52174B00C04ABE /* FullTestView.swift */; };
D353FB5D2A52174C00C04ABE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D353FB5C2A52174C00C04ABE /* Assets.xcassets */; };
D353FB602A52174C00C04ABE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D353FB5F2A52174C00C04ABE /* Preview Assets.xcassets */; };
D353FB6A2A52174C00C04ABE /* LazyPagerExampleAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB692A52174C00C04ABE /* LazyPagerExampleAppTests.swift */; };
D353FB742A52174C00C04ABE /* ImageScrollViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB732A52174C00C04ABE /* ImageScrollViewUITests.swift */; };
D353FB762A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D353FB752A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift */; };
D367DA132A59E930004497D4 /* LazyPager in Frameworks */ = {isa = PBXBuildFile; productRef = D367DA122A59E930004497D4 /* LazyPager */; };
D3776B5F2CF5658500AFB89D /* AnimatedPagerControlsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3776B5E2CF5658500AFB89D /* AnimatedPagerControlsExample.swift */; };
D38D65E32C62E4B900AA140E /* LazyPager in Frameworks */ = {isa = PBXBuildFile; productRef = D38D65E22C62E4B900AA140E /* LazyPager */; };
D3B3AEAC2DBD500800AC1E33 /* EnvironmentExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3B3AEAB2DBD500500AC1E33 /* EnvironmentExample.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
D353FB662A52174C00C04ABE /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D353FB4D2A52174B00C04ABE /* Project object */;
proxyType = 1;
remoteGlobalIDString = D353FB542A52174B00C04ABE;
remoteInfo = ImageScrollView;
};
D353FB702A52174C00C04ABE /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D353FB4D2A52174B00C04ABE /* Project object */;
proxyType = 1;
remoteGlobalIDString = D353FB542A52174B00C04ABE;
remoteInfo = ImageScrollView;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
D309D37F2E88B66E007A6FDC /* InsetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetTest.swift; sourceTree = "<group>"; };
D33BC0512B69E6EE004B4338 /* SimpleExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleExample.swift; sourceTree = "<group>"; };
D33F96FC2C62F582004D934A /* VerticalMediaPager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalMediaPager.swift; sourceTree = "<group>"; };
D353FB552A52174B00C04ABE /* LazyPagerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LazyPagerExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
D353FB582A52174B00C04ABE /* LazyPagerExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyPagerExampleApp.swift; sourceTree = "<group>"; };
D353FB5A2A52174B00C04ABE /* FullTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullTestView.swift; sourceTree = "<group>"; };
D353FB5C2A52174C00C04ABE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D353FB5F2A52174C00C04ABE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
D353FB652A52174C00C04ABE /* LazyPagerExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LazyPagerExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D353FB692A52174C00C04ABE /* LazyPagerExampleAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyPagerExampleAppTests.swift; sourceTree = "<group>"; };
D353FB6F2A52174C00C04ABE /* LazyPagerExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LazyPagerExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D353FB732A52174C00C04ABE /* ImageScrollViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScrollViewUITests.swift; sourceTree = "<group>"; };
D353FB752A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScrollViewUITestsLaunchTests.swift; sourceTree = "<group>"; };
D3776B5E2CF5658500AFB89D /* AnimatedPagerControlsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedPagerControlsExample.swift; sourceTree = "<group>"; };
D38D65E02C62E47C00AA140E /* LazyPager */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = LazyPager; path = ..; sourceTree = "<group>"; };
D3B3AEAB2DBD500500AC1E33 /* EnvironmentExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentExample.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D353FB522A52174B00C04ABE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D38D65E32C62E4B900AA140E /* LazyPager in Frameworks */,
D367DA132A59E930004497D4 /* LazyPager in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D353FB622A52174C00C04ABE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D353FB6C2A52174C00C04ABE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
D353FB4C2A52174B00C04ABE = {
isa = PBXGroup;
children = (
D353FB572A52174B00C04ABE /* LazyPagerExampleApp */,
D353FB682A52174C00C04ABE /* LazyPagerExampleAppTests */,
D353FB722A52174C00C04ABE /* LazyPagerExampleAppUITests */,
D353FB562A52174B00C04ABE /* Products */,
D367DA112A59E930004497D4 /* Frameworks */,
);
sourceTree = "<group>";
};
D353FB562A52174B00C04ABE /* Products */ = {
isa = PBXGroup;
children = (
D353FB552A52174B00C04ABE /* LazyPagerExample.app */,
D353FB652A52174C00C04ABE /* LazyPagerExampleTests.xctest */,
D353FB6F2A52174C00C04ABE /* LazyPagerExampleUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
D353FB572A52174B00C04ABE /* LazyPagerExampleApp */ = {
isa = PBXGroup;
children = (
D3B3AEAB2DBD500500AC1E33 /* EnvironmentExample.swift */,
D353FB582A52174B00C04ABE /* LazyPagerExampleApp.swift */,
D33F96FC2C62F582004D934A /* VerticalMediaPager.swift */,
D33BC0512B69E6EE004B4338 /* SimpleExample.swift */,
D309D37F2E88B66E007A6FDC /* InsetTest.swift */,
D3776B5E2CF5658500AFB89D /* AnimatedPagerControlsExample.swift */,
D353FB5A2A52174B00C04ABE /* FullTestView.swift */,
D353FB5C2A52174C00C04ABE /* Assets.xcassets */,
D353FB5E2A52174C00C04ABE /* Preview Content */,
);
path = LazyPagerExampleApp;
sourceTree = "<group>";
};
D353FB5E2A52174C00C04ABE /* Preview Content */ = {
isa = PBXGroup;
children = (
D353FB5F2A52174C00C04ABE /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
D353FB682A52174C00C04ABE /* LazyPagerExampleAppTests */ = {
isa = PBXGroup;
children = (
D353FB692A52174C00C04ABE /* LazyPagerExampleAppTests.swift */,
);
path = LazyPagerExampleAppTests;
sourceTree = "<group>";
};
D353FB722A52174C00C04ABE /* LazyPagerExampleAppUITests */ = {
isa = PBXGroup;
children = (
D353FB732A52174C00C04ABE /* ImageScrollViewUITests.swift */,
D353FB752A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift */,
);
path = LazyPagerExampleAppUITests;
sourceTree = "<group>";
};
D367DA112A59E930004497D4 /* Frameworks */ = {
isa = PBXGroup;
children = (
D38D65E02C62E47C00AA140E /* LazyPager */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
D353FB542A52174B00C04ABE /* LazyPagerExample */ = {
isa = PBXNativeTarget;
buildConfigurationList = D353FB792A52174C00C04ABE /* Build configuration list for PBXNativeTarget "LazyPagerExample" */;
buildPhases = (
D353FB512A52174B00C04ABE /* Sources */,
D353FB522A52174B00C04ABE /* Frameworks */,
D353FB532A52174B00C04ABE /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = LazyPagerExample;
packageProductDependencies = (
D367DA122A59E930004497D4 /* LazyPager */,
D38D65E22C62E4B900AA140E /* LazyPager */,
);
productName = ImageScrollView;
productReference = D353FB552A52174B00C04ABE /* LazyPagerExample.app */;
productType = "com.apple.product-type.application";
};
D353FB642A52174C00C04ABE /* LazyPagerExampleTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = D353FB7C2A52174C00C04ABE /* Build configuration list for PBXNativeTarget "LazyPagerExampleTests" */;
buildPhases = (
D353FB612A52174C00C04ABE /* Sources */,
D353FB622A52174C00C04ABE /* Frameworks */,
D353FB632A52174C00C04ABE /* Resources */,
);
buildRules = (
);
dependencies = (
D353FB672A52174C00C04ABE /* PBXTargetDependency */,
);
name = LazyPagerExampleTests;
productName = ImageScrollViewTests;
productReference = D353FB652A52174C00C04ABE /* LazyPagerExampleTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
D353FB6E2A52174C00C04ABE /* LazyPagerExampleUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = D353FB7F2A52174C00C04ABE /* Build configuration list for PBXNativeTarget "LazyPagerExampleUITests" */;
buildPhases = (
D353FB6B2A52174C00C04ABE /* Sources */,
D353FB6C2A52174C00C04ABE /* Frameworks */,
D353FB6D2A52174C00C04ABE /* Resources */,
);
buildRules = (
);
dependencies = (
D353FB712A52174C00C04ABE /* PBXTargetDependency */,
);
name = LazyPagerExampleUITests;
productName = ImageScrollViewUITests;
productReference = D353FB6F2A52174C00C04ABE /* LazyPagerExampleUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
D353FB4D2A52174B00C04ABE /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1420;
TargetAttributes = {
D353FB542A52174B00C04ABE = {
CreatedOnToolsVersion = 14.2;
};
D353FB642A52174C00C04ABE = {
CreatedOnToolsVersion = 14.2;
TestTargetID = D353FB542A52174B00C04ABE;
};
D353FB6E2A52174C00C04ABE = {
CreatedOnToolsVersion = 14.2;
TestTargetID = D353FB542A52174B00C04ABE;
};
};
};
buildConfigurationList = D353FB502A52174B00C04ABE /* Build configuration list for PBXProject "LazyPagerExample" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = D353FB4C2A52174B00C04ABE;
packageReferences = (
D38D65E12C62E4B900AA140E /* XCLocalSwiftPackageReference "../" */,
);
productRefGroup = D353FB562A52174B00C04ABE /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
D353FB542A52174B00C04ABE /* LazyPagerExample */,
D353FB642A52174C00C04ABE /* LazyPagerExampleTests */,
D353FB6E2A52174C00C04ABE /* LazyPagerExampleUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
D353FB532A52174B00C04ABE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D353FB602A52174C00C04ABE /* Preview Assets.xcassets in Resources */,
D353FB5D2A52174C00C04ABE /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D353FB632A52174C00C04ABE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D353FB6D2A52174C00C04ABE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
D353FB512A52174B00C04ABE /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D353FB5B2A52174B00C04ABE /* FullTestView.swift in Sources */,
D33F96FD2C62F582004D934A /* VerticalMediaPager.swift in Sources */,
D3776B5F2CF5658500AFB89D /* AnimatedPagerControlsExample.swift in Sources */,
D33BC0522B69E6EE004B4338 /* SimpleExample.swift in Sources */,
D3B3AEAC2DBD500800AC1E33 /* EnvironmentExample.swift in Sources */,
D353FB592A52174B00C04ABE /* LazyPagerExampleApp.swift in Sources */,
D309D3802E88B66E007A6FDC /* InsetTest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D353FB612A52174C00C04ABE /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D353FB6A2A52174C00C04ABE /* LazyPagerExampleAppTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D353FB6B2A52174C00C04ABE /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D353FB762A52174C00C04ABE /* ImageScrollViewUITestsLaunchTests.swift in Sources */,
D353FB742A52174C00C04ABE /* ImageScrollViewUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
D353FB672A52174C00C04ABE /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D353FB542A52174B00C04ABE /* LazyPagerExample */;
targetProxy = D353FB662A52174C00C04ABE /* PBXContainerItemProxy */;
};
D353FB712A52174C00C04ABE /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D353FB542A52174B00C04ABE /* LazyPagerExample */;
targetProxy = D353FB702A52174C00C04ABE /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
D353FB772A52174C00C04ABE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
D353FB782A52174C00C04ABE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
D353FB7A2A52174C00C04ABE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"LazyPagerExampleApp/Preview Content\"";
DEVELOPMENT_TEAM = BX46265734;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollView;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
D353FB7B2A52174C00C04ABE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"LazyPagerExampleApp/Preview Content\"";
DEVELOPMENT_TEAM = BX46265734;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollView;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
D353FB7D2A52174C00C04ABE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = BX46265734;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LazyPagerExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LazyPagerExample";
};
name = Debug;
};
D353FB7E2A52174C00C04ABE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = BX46265734;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LazyPagerExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LazyPagerExample";
};
name = Release;
};
D353FB802A52174C00C04ABE /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = BX46265734;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = ImageScrollView;
};
name = Debug;
};
D353FB812A52174C00C04ABE /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = BX46265734;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dateit.ImageScrollViewUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = ImageScrollView;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
D353FB502A52174B00C04ABE /* Build configuration list for PBXProject "LazyPagerExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D353FB772A52174C00C04ABE /* Debug */,
D353FB782A52174C00C04ABE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D353FB792A52174C00C04ABE /* Build configuration list for PBXNativeTarget "LazyPagerExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D353FB7A2A52174C00C04ABE /* Debug */,
D353FB7B2A52174C00C04ABE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D353FB7C2A52174C00C04ABE /* Build configuration list for PBXNativeTarget "LazyPagerExampleTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D353FB7D2A52174C00C04ABE /* Debug */,
D353FB7E2A52174C00C04ABE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D353FB7F2A52174C00C04ABE /* Build configuration list for PBXNativeTarget "LazyPagerExampleUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D353FB802A52174C00C04ABE /* Debug */,
D353FB812A52174C00C04ABE /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
D38D65E12C62E4B900AA140E /* XCLocalSwiftPackageReference "../" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
D367DA122A59E930004497D4 /* LazyPager */ = {
isa = XCSwiftPackageProductDependency;
productName = LazyPager;
};
D38D65E22C62E4B900AA140E /* LazyPager */ = {
isa = XCSwiftPackageProductDependency;
productName = LazyPager;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = D353FB4D2A52174B00C04ABE /* Project object */;
}
================================================
FILE: Examples/LazyPagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:/Users/brian/dev/LazyPager/Examples/LazyPagerExample.xcodeproj">
</FileRef>
</Workspace>
================================================
FILE: Examples/LazyPagerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
================================================
FILE: Examples/LazyPagerExampleApp/AnimatedPagerControlsExample.swift
================================================
import SwiftUI
import LazyPager
struct AnimatedPagerControlsExample: View {
@State var data = [
"nora1",
"nora2",
"nora3",
"nora4",
"nora5",
"nora6",
]
@State var show = false
@State var index = 0
var body: some View {
VStack {
LazyPager(data: data, page: $index) { element in
Image(element)
.resizable()
.aspectRatio(contentMode: .fit)
}
HStack(spacing: 20) {
Button("First") {
withAnimation {
index = 0
}
}
Button("Prev") {
withAnimation {
if index > 0 {
index -= 1
}
}
}
Button("Next") {
withAnimation {
if index < data.count {
index += 1
}
}
}
Button("Last") {
withAnimation {
index = data.count - 1
}
}
}
}
}
}
struct AnimatedPagerControlsExample_Previews: PreviewProvider {
static var previews: some View {
AnimatedPagerControlsExample()
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/nora1.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "356181627_737281678149026_5519646735590788375_n.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/nora2.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "356184881_810974757010572_166165563303848404_n.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/nora3.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "356184996_1504506290292039_6439519590743317419_n.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/nora4.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "356187567_797832131883666_8693445044613773171_n.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/nora5.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "356198313_803291668248047_1588179413198578920_n.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Assets.xcassets/nora6.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "358743821_933760767702238_5920729387861732707_n.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/EnvironmentExample.swift
================================================
//
// EnvironmentExample.swift
// LazyPagerExample
//
// Created by Brian Floersch on 4/26/25.
//
import SwiftUI
import LazyPager
struct SubView: View {
@EnvironmentObject var textHolder: TextHolder
@Environment(\.customValue) var customValue
var parentText: String
var body: some View {
VStack {
Text("\(textHolder.str) \(parentText)")
.font(.title)
.padding()
Text("Environment value: \(customValue)")
.font(.subheadline)
.padding()
}
}
}
struct EnvironmentExample: View {
@State var data = [
"nora1",
"nora2",
"nora3",
"nora4",
"nora5",
"nora6",
]
@State var show = false
var body: some View {
ZStack {
LazyPager(data: data) { element in
SubView(parentText: element)
}
}
}
}
class TextHolder: ObservableObject {
let str: String
init(str: String) {
self.str = str
}
}
private struct CustomEnvironmentKey: EnvironmentKey {
static let defaultValue: String = "default value"
}
extension EnvironmentValues {
var customValue: String {
get { self[CustomEnvironmentKey.self] }
set { self[CustomEnvironmentKey.self] = newValue }
}
}
#Preview {
EnvironmentExample()
.environmentObject(TextHolder(str: "hello world"))
.environment(\.customValue, "custom environment value")
}
================================================
FILE: Examples/LazyPagerExampleApp/FullTestView.swift
================================================
//
// ContentView.swift
// LazyPager
//
// Created by Brian Floersch on 7/2/23.
//
import SwiftUI
import LazyPager
struct Foo {
let id = UUID()
var img: String
let idx: Int
}
struct FullTestView: View {
var direction: Direction
@State var data = [
Foo(img: "nora1", idx: 0),
Foo(img: "nora2", idx: 1),
Foo(img: "nora3", idx: 2),
Foo(img: "nora4", idx: 3),
Foo(img: "nora5", idx: 4),
Foo(img: "nora6", idx: 5),
Foo(img: "nora1", idx: 6),
Foo(img: "nora2", idx: 7),
Foo(img: "nora3", idx: 8),
Foo(img: "nora4", idx: 9),
Foo(img: "nora5", idx: 10),
Foo(img: "nora6", idx: 11),
]
@Binding var show: Bool
@State var opacity: CGFloat = 1
@State var index = 0
@State var loadPager = false
var body: some View {
VStack {
LazyPager(data: data, page: $index, direction: direction) { element in
ZStack {
Image(element.img)
.resizable()
.aspectRatio(contentMode: .fit)
VStack {
Text("\(index) \(element.idx) \(data.count - 1)")
.foregroundColor(.black)
.background(.white)
}
}
}
.zoomable(min: 1, max: 5)
.onDismiss(backgroundOpacity: $opacity) {
show = false
}
.onTap {
print("tap")
}
.onDoubleTap {
print("double tap")
}
.shouldLoadMore(on: .lastElement(minus: 2)) {
data.append(Foo(img: "nora4", idx: data.count))
}
.overscroll { position in
if position == .beginning {
print("Swiped past beginning")
} else {
print("Swiped past end")
}
}
.onDrag {
print("Drag")
}
.pageSpacing(10)
.background(.black.opacity(opacity))
.background(ClearFullScreenBackground())
.ignoresSafeArea()
VStack {
HStack(spacing: 30) {
Button("-") {
index -= 1
}
VStack(spacing: 10) {
Button("append") {
data.append(Foo(img: "nora4", idx: data.count + 1))
}
Button("replace") {
data[0] = Foo(img: "nora4", idx: data.count + 1)
}
Button("update") {
data[0].img = "nora5"
}
}
VStack(spacing: 10) {
Button("del first") {
data.remove(at: 0)
index -= 1
}
Button("del last") {
data.remove(at: data.count - 1)
}
Button("jmp") {
index = 10
}
}
Button("+") {
index += 1
}
}
}
.frame(maxWidth: .infinity)
.background(.white)
}
}
}
struct FullTestView_Previews: PreviewProvider {
static var previews: some View {
FullTestView(direction: .horizontal, show: .constant(true))
}
}
================================================
FILE: Examples/LazyPagerExampleApp/InsetTest.swift
================================================
import SwiftUI
import LazyPager
struct InsetTest: View {
@State var data = [
"nora1",
"nora2",
"nora3",
"nora4",
"nora5",
"nora6",
]
@State var show = false
@State var ignoreSafeArea = true
var body: some View {
NavigationStack {
ZStack {
if ignoreSafeArea {
LazyPager(data: data) { element in
Image(element)
.resizable()
.aspectRatio(contentMode: .fit)
}
.ignoresSafeArea()
VStack {
}
.frame(width: 300, height: 300)
.background(.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
} else {
LazyPager(data: data) { element in
Image(element)
.resizable()
.aspectRatio(contentMode: .fit)
}
VStack {
}
.frame(width: 300, height: 300)
.background(.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
VStack {
Spacer()
Toggle("ignore safe area", isOn: $ignoreSafeArea)
.padding()
}
}
}
}
}
#Preview {
InsetTest()
}
================================================
FILE: Examples/LazyPagerExampleApp/LazyPagerExampleApp.swift
================================================
//
// LazyPagerApp.swift
// LazyPager
//
// Created by Brian Floersch on 7/2/23.
//
import SwiftUI
@main
struct LazyPagerApp: App {
@State var showFull = false
var body: some Scene {
WindowGroup {
NavigationStack {
VStack(spacing: 20) {
NavigationLink(destination: SimpleExample()) {
Text("Simple Example")
}
NavigationLink(destination: InsetTest()) {
Text("Inset test")
}
NavigationLink(destination: EnvironmentExample()
.environmentObject(TextHolder(str: "hello world"))
.environment(\.customValue, "custom environment value")
) {
Text("Environment Example")
}
NavigationLink(destination: AnimatedPagerControlsExample()) {
Text("Animated Pager Controls Example")
}
Button("full Test View horizontal") {
showFull.toggle()
}
NavigationLink(destination: FullTestView(direction: .vertical, show: .constant(true))) {
Text("Full Test View vertical")
}
NavigationLink(destination: VerticalMediaPager()) {
Text("Vertical media pager sample")
}
}
}
.fullScreenCover(isPresented: $showFull) {
FullTestView(direction: .horizontal, show: $showFull)
}
}
}
}
================================================
FILE: Examples/LazyPagerExampleApp/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Examples/LazyPagerExampleApp/SimpleExample.swift
================================================
import SwiftUI
import LazyPager
struct SimpleExample: View {
@State var data = [
"nora1",
"nora2",
"nora3",
"nora4",
"nora5",
"nora6",
]
@State var show = false
var body: some View {
LazyPager(data: data) { element in
Image(element)
.resizable()
.aspectRatio(contentMode: .fit)
}
.onDismiss {
}
}
}
struct SimpleExample_Previews: PreviewProvider {
static var previews: some View {
SimpleExample()
}
}
================================================
FILE: Examples/LazyPagerExampleApp/VerticalMediaPager.swift
================================================
import SwiftUI
import LazyPager
struct VerticalMediaPager: View {
@State var data = [
"nora1",
"nora2",
"nora3",
"nora4",
"nora5",
"nora6",
]
@State var show = true
var body: some View {
ZStack {
LazyPager(data: data, direction: .vertical) { element in
Image(element)
.resizable()
.aspectRatio(contentMode: .fill)
}
.overscroll {
if $0 == .beginning {
print("Swiped past beginning")
} else if $0 == .end {
print("Swiped past end")
}
}
.ignoresSafeArea()
VStack(alignment: .leading) {
HStack {
Spacer()
VStack(spacing: 30) {
Spacer()
imgButton("heart.fill")
imgButton("text.bubble.fill")
imgButton("bookmark.fill")
imgButton("arrow.turn.up.right")
}
.padding(.bottom, 20)
}
.padding()
Spacer()
HStack {
VStack {
Text("CatTok")
.frame(maxWidth: .infinity, alignment: .leading)
.font(.title2)
Text("Nora is an adorable cat")
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Text("😸")
.font(.title)
.padding(5)
.background(.pink.opacity(0.8))
.clipShape(Circle())
}
.padding()
.foregroundColor(.white)
.background(.black.opacity(0.5))
}
}
}
@ViewBuilder
func imgButton(_ name: String) -> some View {
Image(systemName: name)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.foregroundColor(.white.opacity(0.9))
}
}
#Preview {
VerticalMediaPager()
}
================================================
FILE: Examples/LazyPagerExampleAppTests/LazyPagerExampleAppTests.swift
================================================
//
// LazyPagerTests.swift
// LazyPagerTests
//
// Created by Brian Floersch on 7/2/23.
//
import XCTest
@testable import LazyPager
final class LazyPagerTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
================================================
FILE: Examples/LazyPagerExampleAppUITests/ImageScrollViewUITests.swift
================================================
//
// LazyPagerUITests.swift
// LazyPagerUITests
//
// Created by Brian Floersch on 7/2/23.
//
import XCTest
final class LazyPagerUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// 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.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}
================================================
FILE: Examples/LazyPagerExampleAppUITests/ImageScrollViewUITestsLaunchTests.swift
================================================
//
// LazyPagerUITestsLaunchTests.swift
// LazyPagerUITests
//
// Created by Brian Floersch on 7/2/23.
//
import XCTest
final class LazyPagerUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Brian Floersch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "LazyPager",
platforms: [
.iOS(.v15)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "LazyPager",
targets: ["LazyPager"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "LazyPager",
dependencies: []),
.testTarget(
name: "LazyPagerTests",
dependencies: ["LazyPager"]),
]
)
================================================
FILE: README.md
================================================
# LazyPager for SwiftUI
A buttery smooth, lazy loaded, panning, zooming, and gesture dismissible view pager view for SwiftUI.
The 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.
### Horizontal
<p align="center">
<img src="https://github.com/gh123man/LazyPager/assets/959778/a82da8c3-9d65-4782-8fd7-40cc598e16da" alt="animated" />
</p>
The 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`.
### Vertical
<p align="center">
<img src="https://github.com/user-attachments/assets/21679506-c2ad-491c-8fe8-13fbd2b0aa2a" alt="animated" />
</p>
The above example [can be found in the example project.](https://github.com/gh123man/SwiftUI-LazyPager/blob/master/Examples/LazyPagerExampleApp/VerticalMediaPager.swift)
# Usage
## Add the Swift Package
1. Right click on your project -> `Add Package`
2. In the search bar paste: `https://github.com/gh123man/LazyPager`
3. Click `Add Package`
Or add the package to your `Package.swift` if your project is a Swift package.
## Examples
### Simple Example
A simple image pager that displays images by name from your app assets.
```swift
@State var data = [ ... ]
var body: some View {
LazyPager(data: data) { element in
Image(element)
.resizable()
.aspectRatio(contentMode: .fit)
}
}
```
That's it!
### Detailed Example
```swift
@State var data = [ ... ]
@State var show = true
@State var opacity: CGFloat = 1 // Dismiss gesture background opacity
@State var index = 0
var body: some View {
Button("Open") {
show.toggle()
}
.fullScreenCover(isPresented: $show) {
// Provide any list of data and bind to an index
LazyPager(data: data, page: $index) { element in
// Supports any kind of view - not only images
Image(element)
.resizable()
.aspectRatio(contentMode: .fit)
}
// Make the content zoomable
.zoomable(min: 1, max: 5)
// Enable the swipe to dismiss gesture and background opacity control
.onDismiss(backgroundOpacity: $opacity) {
show = false
}
// Handle single tap gestures
.onTap {
print("tap")
}
// Get notified when to load more content
.shouldLoadMore {
data.append("foobar")
}
// Get notified when swiping past the beginning or end of the list
.overscroll { position in
if position == .beginning {
print("Swiped past beginning")
} else {
print("Swiped past end")
}
}
// Handle double tap gestures
.onDoubleTap {
print("double tap")
}
// Handle drag events initiated by the user
.onDrag {
print("Drag")
}
// Set the spacing between pages
.pageSpacing(20)
// Set the background color with the drag opacity control
.background(.black.opacity(opacity))
// A special included modifier to help make fullScreenCover transparent
.background(ClearFullScreenBackground())
// Works with safe areas or ignored safe areas
.ignoresSafeArea()
}
}
```
#### Vertical paging
```swift
@State var data = [ ... ]
var body: some View {
LazyPager(data: data, direction: .vertical) { element in
Image(element)
.resizable()
.aspectRatio(contentMode: .fill)
}
}
```
For 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)
# Features
- All content is lazy loaded. By default content is pre-loaded 3 elements ahead and behind the current index.
- Display any kind of content - not just images!
- Horizontal or Vertical paging.
- Lazy loaded views are disposed when they are outside of the pre-load frame to conserve resources.
- Enable zooming and panning with `.zoomable(min: CGFloat, max: CGFloat)`.
- Double tap to zoom is also supported through `.zoomable` modifier.
- Notifies when to load more content with `.shouldLoadMore`.
- Notifies when you swipe past the beginning or end of data with `.overscroll`.
- Animate page transitions by using `withAnimation` when changing the page index.
- Works with `.ignoresSafeArea()` (or not) to get a true full screen view.
- Drag to dismiss is supported with `.onDismiss` - Supply a binding opacity value to control the background opacity during the transition.
- Tap events are handled internally, so use `.onTap` to handle single taps (useful for hiding and showing UI).
- Use `.onDoubleTap` to get notified on double taps.
- Use `.settings` to [modify advanced settings](https://github.com/gh123man/SwiftUI-LazyPager/blob/master/Sources/LazyPager/LazyPager.swift#L76).
- Use `.absoluteContentPosition` to subscribe to content position updates (the index + the offset while paging)
- Use `.onZoom` to get notified of the current zoom level
- Use `.onDrag` to handle drag events when the user interacts with the view. No triggered when page is changed programmatically.
- Use `.pageSpacing(CGFloat)` to set the spacing between pages.
# Detailed usage
## Working with `fullScreenCover`
`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.
## Double tap to zoom
You 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.
## Dismiss gesture handling
By 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.
To control the dismiss opacity of a custom background, use a `Binding<CGFloat>` like `.onDismiss(backgroundOpacity: $opacity) {` to fade out your custom background.
================================================
FILE: Sources/LazyPager/ClearFullScreenBackground.swift
================================================
//
// ClearFullScreenBackground.swift
//
//
// Created by Brian Floersch on 7/8/23.
//
import Foundation
import SwiftUI
import UIKit
public struct ClearFullScreenBackground: UIViewRepresentable {
public init() { }
public func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
public func updateUIView(_ uiView: UIView, context: Context) {}
}
================================================
FILE: Sources/LazyPager/Collection+Extensions.swift
================================================
//
// Collection+Extensons.swift
//
//
// Created by Brian Floersch on 7/8/23.
//
import Foundation
extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
================================================
FILE: Sources/LazyPager/LazyPager.swift
================================================
//
// LazyPager.swift
// LazyPager
//
// Created by Brian Floersch on 7/6/23.
//
import Foundation
import UIKit
import SwiftUI
public enum LoadMore {
case lastElement(minus: Int = 0)
}
public enum DoubleTap {
case disabled
case scale(CGFloat)
}
public enum Direction {
case horizontal
case vertical
}
public enum ListPosition {
case beginning
case end
}
public enum ZoomConfig {
case disabled
case custom(min: CGFloat, max: CGFloat, doubleTap: DoubleTap)
}
public struct Config<Element> {
/// binding variable to control a custom background opacity. LazyPager is transparent by default
public var backgroundOpacity: Binding<CGFloat>?
/// Called when the view is done dismissing - dismiss gesture is disabled if nil
public var dismissCallback: (() -> ())?
/// Called when tapping once
public var tapCallback: (() -> ())?
/// Called when tapping twice
public var doubleTapCallback: (() -> ())?
/// Called when dragging begins
public var dragCallback: (() -> ())?
/// The offset used to trigger load loadMoreCallback
public var loadMoreOn: LoadMore = .lastElement(minus: 3)
/// Called when more content should be loaded
public var loadMoreCallback: (() -> ())?
/// Direction of the pager
public var direction : Direction = .horizontal
/// Called whent the end of data is reached and the user tries to swipe again
public var overscrollCallback: ((ListPosition) -> ())?
/// The element index + the offset while paging
public var absoluteContentPosition: Binding<CGFloat>?
/// Called every view update to get the zoom config
public var zoomConfigGetter: (Element) -> ZoomConfig = { _ in .disabled }
/// Called while zooming to provide the current zoom level for an element
public var onZoomHandler: ((Element, CGFloat) -> ())?
/// The spacing between pages. Defaults to 0.
public var pageSpacing: CGFloat = 0
/// Advanced settings (only accessible via .settings)
/// How may out of view pages to load in advance (forward and backwards)
public var preloadAmount: Int = 3
/// Minimum swipe velocity needed to trigger a dismiss
public var dismissVelocity: CGFloat = 1.3
/// the minimum % (between 0 and 1) you need to drag to trigger a dismiss
public var dismissTriggerOffset: CGFloat = 0.1
/// How long to animate the dismiss once done dragging
public var dismissAnimationLength: CGFloat = 0.2
/// Cancel SwiftUI animations. Default to true because the dismiss gesture is already animated.
/// Stacking animations can cause undesirable behavior
public var shouldCancelSwiftUIAnimationsOnDismiss = true
/// At what drag % (between 0 and 1) the background should be fully transparent
public var fullFadeOnDragAt: CGFloat = 0.2
/// The minimum scroll distance the in which the pinch gesture is enabled
public var pinchGestureEnableOffset: Double = 10
/// % ammount (from 0-1) of overscroll needed to call overscrollCallback
public var overscrollThreshold: Double = 0.15
}
public struct LazyPager<Element, DataCollecton: RandomAccessCollection, Content: View> where DataCollecton.Index == Int, DataCollecton.Element == Element {
private var viewLoader: (Element) -> Content
private var data: DataCollecton
@State private var defaultPageInternal = 0
private var providedPage: Binding<Int>?
private var page: Binding<Int> {
providedPage ?? Binding(
get: { defaultPageInternal },
set: { defaultPageInternal = $0 }
)
}
var config = Config<Element>()
public init(data: DataCollecton,
page: Binding<Int>? = nil,
direction: Direction = .horizontal,
@ViewBuilder content: @escaping (Element) -> Content) {
self.data = data
self.providedPage = page
self.viewLoader = content
self.config.direction = direction
}
}
public extension LazyPager {
func onDismiss(backgroundOpacity: Binding<CGFloat>? = nil, _ callback: @escaping () -> ()) -> LazyPager {
guard config.direction == .horizontal else {
return self
}
var this = self
this.config.backgroundOpacity = backgroundOpacity
this.config.dismissCallback = callback
return this
}
func onTap(_ callback: @escaping () -> ()) -> LazyPager {
var this = self
this.config.tapCallback = callback
return this
}
func onDoubleTap(_ callback: @escaping () -> ()) -> LazyPager {
var this = self
this.config.doubleTapCallback = callback
return this
}
func onDrag(_ callback: @escaping () -> ()) -> LazyPager {
var this = self
this.config.dragCallback = callback
return this
}
func pageSpacing(_ spacing: CGFloat) -> LazyPager {
var this = self
this.config.pageSpacing = spacing
return this
}
func shouldLoadMore(on: LoadMore = .lastElement(minus: 3), _ callback: @escaping () -> ()) -> LazyPager {
var this = self
this.config.loadMoreOn = on
this.config.loadMoreCallback = callback
return this
}
func zoomable(min: CGFloat, max: CGFloat, doubleTapGesture: DoubleTap = .scale(0.5)) -> LazyPager {
var this = self
this.config.zoomConfigGetter = { _ in
return .custom(min: min, max: max, doubleTap: doubleTapGesture)
}
return this
}
func zoomable(onElement: @escaping (Element) -> ZoomConfig) -> LazyPager {
var this = self
this.config.zoomConfigGetter = onElement
return this
}
func settings(_ adjust: @escaping (inout Config<Element>) -> ()) -> LazyPager {
var this = self
adjust(&this.config)
return this
}
func overscroll(_ callback: @escaping (ListPosition) -> ()) -> LazyPager {
var this = self
this.config.overscrollCallback = callback
return this
}
func absoluteContentPosition(_ absoluteContentPosition: Binding<CGFloat>? = nil) -> LazyPager {
guard config.direction == .horizontal else {
return self
}
var this = self
this.config.absoluteContentPosition = absoluteContentPosition
return this
}
func onZoom(_ onZoomHandler: @escaping (Element, CGFloat) -> ()) -> LazyPager {
var this = self
this.config.onZoomHandler = onZoomHandler
return this
}
}
extension LazyPager: UIViewControllerRepresentable {
public func makeUIViewController(context: Context) -> ViewDataProvider<Content, DataCollecton, Element> {
let provider = ViewDataProvider(data: data,
page: page,
config: config,
viewLoader: viewLoader)
DispatchQueue.main.async {
provider.goToPage(page.wrappedValue, animated: false)
}
return provider
}
public func updateUIViewController(_ uiViewController: ViewDataProvider<Content, DataCollecton, Element>, context: Context) {
uiViewController.viewLoader = viewLoader
uiViewController.data = data
defer { uiViewController.reloadViews() }
if page.wrappedValue != uiViewController.pagerView.currentIndex {
// Index was explicitly updated
uiViewController.goToPage(page.wrappedValue, animated: context.transaction.animation != nil)
}
if page.wrappedValue >= data.count {
uiViewController.goToPage(data.count - 1, animated: false)
} else if page.wrappedValue < 0 {
uiViewController.goToPage(0, animated: false)
}
}
}
================================================
FILE: Sources/LazyPager/Math.swift
================================================
//
// Math.swift
//
//
// Created by Brian Floersch on 7/8/23.
//
import Foundation
func lerp(from: CGFloat, to: CGFloat, by: CGFloat) -> CGFloat {
return from * (1 - by) + to * by
}
func normalize(from min: CGFloat, at val: CGFloat, to max: CGFloat) -> CGFloat {
let v = (val - min) / (max - min)
return v < 0 ? 0 : v > 1 ? 1 : v
}
================================================
FILE: Sources/LazyPager/PagerView.swift
================================================
//
// PagerView.swift
//
//
// Created by Brian Floersch on 7/8/23.
//
import Foundation
import UIKit
import SwiftUI
protocol ViewLoader: AnyObject {
associatedtype Element
associatedtype Content: View
var dataCount: Int { get }
func loadView(at: Int) -> ZoomableView<Element, Content>?
func updateHostedView(for zoomableView: ZoomableView<Element, Content>)
}
class PagerView<Element, Loader: ViewLoader, Content: View>: UIScrollView, UIScrollViewDelegate where Loader.Element == Element, Loader.Content == Content {
var pageSpacing: CGFloat {
config.pageSpacing
}
var isFirstLoad = false
var loadedViews = [ZoomableView<Element, Content>]()
var config: Config<Element>
weak var viewLoader: Loader?
var lastBoundsSize: CGSize?
var isRotating = false
var page: Binding<Int>
var currentIndex: Int = 0 {
didSet {
loadMoreIfNeeded()
}
}
var absoluteOffset: CGFloat {
var absoluteOffset: CGFloat
if config.direction == .horizontal {
absoluteOffset = self.contentOffset.x / self.pageWidth
} else {
absoluteOffset = self.contentOffset.y / self.pageHeight
}
return absoluteOffset
}
var relativeIndex: Int {
if absoluteOffset.isInfinite || absoluteOffset.isNaN {
return 0
}
var idx = Int(round(absoluteOffset))
idx = idx < 0 ? 0 : idx
idx = idx >= loadedViews.count ? loadedViews.count-1 : idx
return idx
}
var currentView: ZoomableView<Element, Content> {
loadedViews[relativeIndex]
}
var pageWidth: CGFloat {
if config.direction == .horizontal {
return frame.width + pageSpacing
}
return frame.width
}
var pageHeight: CGFloat {
if config.direction == .vertical {
return frame.height + pageSpacing
}
return frame.height
}
init(page: Binding<Int>, config: Config<Element>) {
self.currentIndex = page.wrappedValue
self.page = page
self.config = config
super.init(frame: .zero)
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
backgroundColor = .clear
decelerationRate = .fast
delegate = self
contentInsetAdjustmentBehavior = .always
// DEBUG
// backgroundColor = .blue
}
required init?(coder: NSCoder) {
fatalError("Not implemented")
}
public override func layoutSubviews() {
super.layoutSubviews()
if !isFirstLoad {
ensureCurrentPage(animated: false)
isFirstLoad = true
} else if isRotating {
ensureCurrentPage(animated: false)
}
// Ensures insets are updated when the screen rotates
if bounds.size != lastBoundsSize {
updateInsets(animated: lastBoundsSize != nil)
lastBoundsSize = bounds.size
}
}
func computeViewState(immediate: Bool = false) {
delegate = nil
DispatchQueue.main.async {
self.delegate = self
}
if subviews.isEmpty {
for i in currentIndex...(currentIndex + config.preloadAmount) {
if immediate {
appendView(at: i)
} else {
scheduleAppend(at: i)
}
}
for i in ((currentIndex - config.preloadAmount)..<currentIndex).reversed() {
if immediate {
schedulePrepend(at: i)
} else {
prependView(at: i)
}
}
}
if let lastView = loadedViews.last {
let diff = lastView.index - currentIndex
if diff < (config.preloadAmount) {
for i in lastView.index..<(lastView.index + (config.preloadAmount - diff)) {
if immediate {
appendView(at: i + 1)
} else {
scheduleAppend(at: i + 1)
}
}
}
}
if let firstView = loadedViews.first {
let diff = currentIndex - firstView.index
if diff < (config.preloadAmount) {
for i in (firstView.index - (config.preloadAmount - diff)..<firstView.index).reversed() {
if immediate {
schedulePrepend(at: i)
} else {
prependView(at: i)
}
}
}
}
self.removeOutOfFrameViews()
updateInsets(animated: false)
// Debug
// print(self.loadedViews.map { $0.index })
}
func updateInsets(animated: Bool) {
let update = {
self.contentInset = UIEdgeInsets(top: -self.safeAreaInsets.top,
left: -self.safeAreaInsets.left,
bottom: -self.safeAreaInsets.bottom,
right: -self.safeAreaInsets.right)
}
if !animated {
update()
} else {
UIView.animate(withDuration: 0.3) {
update()
}
}
}
func addSubview(_ zoomView: ZoomableView<Element, Content>) {
super.addSubview(zoomView)
NSLayoutConstraint.activate([
zoomView.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor),
zoomView.heightAnchor.constraint(equalTo: frameLayoutGuide.heightAnchor),
])
}
func addFirstView(_ zoomView: ZoomableView<Element, Content>) {
if config.direction == .horizontal {
zoomView.leadingConstraint = zoomView.leadingAnchor.constraint(equalTo: leadingAnchor)
zoomView.trailingConstraint = zoomView.trailingAnchor.constraint(equalTo: trailingAnchor)
zoomView.leadingConstraint?.isActive = true
zoomView.trailingConstraint?.isActive = true
} else {
zoomView.topConstraint = zoomView.topAnchor.constraint(equalTo: topAnchor)
zoomView.bottomConstraint = zoomView.bottomAnchor.constraint(equalTo: bottomAnchor)
zoomView.topConstraint?.isActive = true
zoomView.bottomConstraint?.isActive = true
}
}
func scheduleAppend(at index: Int) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Ensure we are not trying to add a view that has already been loaded
if self.loadedViews.contains(where: { $0.index == index }) {
return
}
self.appendView(at: index)
}
}
func appendView(at index: Int) {
guard let zoomView = viewLoader?.loadView(at: index) else { return }
addSubview(zoomView)
if let lastView = loadedViews.last {
if config.direction == .horizontal {
lastView.trailingConstraint?.isActive = false
lastView.trailingConstraint = nil
zoomView.leadingConstraint = zoomView.leadingAnchor.constraint(equalTo: lastView.trailingAnchor, constant: pageSpacing)
zoomView.trailingConstraint = zoomView.trailingAnchor.constraint(equalTo: trailingAnchor)
zoomView.leadingConstraint?.isActive = true
zoomView.trailingConstraint?.isActive = true
} else {
lastView.bottomConstraint?.isActive = false
lastView.bottomConstraint = nil
zoomView.topConstraint = zoomView.topAnchor.constraint(equalTo: lastView.bottomAnchor, constant: pageSpacing)
zoomView.bottomConstraint = zoomView.bottomAnchor.constraint(equalTo: bottomAnchor)
zoomView.topConstraint?.isActive = true
zoomView.bottomConstraint?.isActive = true
}
} else {
addFirstView(zoomView)
}
loadedViews.append(zoomView)
}
func schedulePrepend(at index: Int) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Ensure we are not trying to add a view that has already been loaded
if self.loadedViews.contains(where: { $0.index == index }) {
return
}
self.prependView(at: index)
}
}
func prependView(at index: Int) {
guard let zoomView = viewLoader?.loadView(at: index) else { return }
addSubview(zoomView)
if let firstView = loadedViews.first {
if config.direction == .horizontal {
firstView.leadingConstraint?.isActive = false
firstView.leadingConstraint = nil
zoomView.leadingConstraint = zoomView.leadingAnchor.constraint(equalTo: leadingAnchor)
zoomView.trailingConstraint = zoomView.trailingAnchor.constraint(equalTo: firstView.leadingAnchor, constant: -pageSpacing)
zoomView.leadingConstraint?.isActive = true
zoomView.trailingConstraint?.isActive = true
} else {
firstView.topConstraint?.isActive = false
firstView.topConstraint = nil
zoomView.topConstraint = zoomView.topAnchor.constraint(equalTo: topAnchor)
zoomView.bottomConstraint = zoomView.bottomAnchor.constraint(equalTo: firstView.topAnchor, constant: -pageSpacing)
zoomView.topConstraint?.isActive = true
zoomView.bottomConstraint?.isActive = true
}
} else {
addFirstView(zoomView)
}
loadedViews.insert(zoomView, at: 0)
if config.direction == .horizontal {
contentOffset.x += pageWidth
} else {
contentOffset.y += pageHeight
}
}
func reloadViews() {
for view in loadedViews {
viewLoader?.updateHostedView(for: view)
}
}
func remove(view: ZoomableView<Element, Content>) {
guard let viewIndex = loadedViews.firstIndex(where: { $0.index == view.index }) else { return }
let viewToDisconnect = loadedViews[viewIndex]
let prevView: ZoomableView<Element, Content>? = loadedViews[safe: viewIndex - 1]
let nextView: ZoomableView<Element, Content>? = loadedViews[safe: viewIndex + 1]
let removedIndex = viewToDisconnect.index
viewToDisconnect.removeFromSuperview()
loadedViews.remove(at: viewIndex)
if config.direction == .horizontal {
if let prevView = prevView, let nextView = nextView {
// Both exist, removing from the middle
prevView.trailingConstraint?.isActive = false
prevView.trailingConstraint = prevView.trailingAnchor.constraint(equalTo: nextView.leadingAnchor, constant: -pageSpacing)
prevView.trailingConstraint?.isActive = true
} else if let prevView = prevView {
// This was the last view
prevView.trailingConstraint?.isActive = false
prevView.trailingConstraint = prevView.trailingAnchor.constraint(equalTo: trailingAnchor)
prevView.trailingConstraint?.isActive = true
} else if let nextView = nextView {
// This was the first view
nextView.leadingConstraint?.isActive = false
nextView.leadingConstraint = nextView.leadingAnchor.constraint(equalTo: leadingAnchor)
nextView.leadingConstraint?.isActive = true
}
if removedIndex < (loadedViews.first?.index ?? 0) {
contentOffset.x -= pageWidth
}
} else {
if let prevView = prevView, let nextView = nextView {
// Both exist, removing from the middle
prevView.bottomConstraint?.isActive = false
prevView.bottomConstraint = prevView.bottomAnchor.constraint(equalTo: nextView.topAnchor, constant: -pageSpacing)
prevView.bottomConstraint?.isActive = true
} else if let prevView = prevView {
// This was the last view
prevView.bottomConstraint?.isActive = false
prevView.bottomConstraint = prevView.bottomAnchor.constraint(equalTo: bottomAnchor)
prevView.bottomConstraint?.isActive = true
} else if let nextView = nextView {
// This was the first view
nextView.topConstraint?.isActive = false
nextView.topConstraint = nextView.topAnchor.constraint(equalTo: topAnchor)
nextView.topConstraint?.isActive = true
}
if removedIndex < (loadedViews.first?.index ?? 0) {
contentOffset.y -= pageHeight
}
}
}
func removeOutOfFrameViews() {
guard let viewLoader = viewLoader else { return }
for view in loadedViews {
if abs(currentIndex - view.index) > config.preloadAmount || view.index >= viewLoader.dataCount {
remove(view: view)
}
}
}
func resizeOutOfBoundsViews() {
for v in loadedViews {
if v.index != currentIndex {
v.zoomScale = 1
}
}
}
func goToPage(_ page: Int, animated: Bool) {
currentIndex = page
DispatchQueue.main.async {
self.computeViewState(immediate: true)
self.ensureCurrentPage(animated: animated)
}
}
func ensureCurrentPage(animated: Bool) {
guard let index = loadedViews.firstIndex(where: { $0.index == currentIndex }) else { return }
if config.direction == .horizontal {
setContentOffset(CGPoint(x: CGFloat(index) * pageWidth, y: contentOffset.y), animated: animated)
} else {
setContentOffset(CGPoint(x: contentOffset.x, y: CGFloat(index) * pageHeight), animated: animated)
}
self.currentView.dismissEnabled = true
}
func loadMoreIfNeeded() {
guard let loadMoreCallback = config.loadMoreCallback else { return }
guard case let .lastElement(offset) = config.loadMoreOn else { return }
guard let viewLoader = viewLoader else { return }
if currentIndex + offset >= viewLoader.dataCount - 1 {
DispatchQueue.main.async {
loadMoreCallback()
}
}
}
func scrollingFinished() {
let newIndex = currentView.index
if currentIndex != newIndex {
currentIndex = newIndex
page.wrappedValue = newIndex
}
computeViewState()
hasNotfiedOverscroll = false
resizeOutOfBoundsViews()
if loadedViews.isEmpty { return }
currentView.dismissEnabled = true
}
// MARK: UISCrollVieDelegate methods
var lastPos: CGFloat = 0
var hasNotfiedOverscroll = false
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
config.dragCallback?()
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !scrollView.isTracking, !isRotating, (currentView.index != page.wrappedValue || page.wrappedValue != currentIndex ) {
currentIndex = currentView.index
page.wrappedValue = currentIndex
}
if let index = loadedViews[safe: relativeIndex]?.index {
config.absoluteContentPosition?.wrappedValue = CGFloat(index) + absoluteOffset - CGFloat(relativeIndex)
}
if !hasNotfiedOverscroll {
if relativeIndex >= loadedViews.count-1, absoluteOffset - CGFloat(relativeIndex) > config.overscrollThreshold {
config.overscrollCallback?(.end)
hasNotfiedOverscroll = true
}
if relativeIndex <= 0, absoluteOffset - CGFloat(relativeIndex) < -config.overscrollThreshold {
config.overscrollCallback?(.beginning)
hasNotfiedOverscroll = true
}
}
if loadedViews.isEmpty { return }
self.currentView.dismissEnabled = false
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let targetPage: CGFloat
let velocityThreshold: CGFloat = 0.5 // a value to tune
if config.direction == .horizontal {
let currentRelativePage = scrollView.contentOffset.x / pageWidth
if velocity.x > velocityThreshold {
// Swiped forward
targetPage = floor(currentRelativePage + 1)
} else if velocity.x < -velocityThreshold {
// Swiped backward
targetPage = ceil(currentRelativePage - 1)
} else {
// No strong swipe, snap to nearest
targetPage = round(currentRelativePage)
}
targetContentOffset.pointee.x = targetPage * pageWidth
} else {
let currentRelativePage = scrollView.contentOffset.y / pageHeight
if velocity.y > velocityThreshold {
targetPage = floor(currentRelativePage + 1)
} else if velocity.y < -velocityThreshold {
targetPage = ceil(currentRelativePage - 1)
} else {
targetPage = round(currentRelativePage)
}
targetContentOffset.pointee.y = targetPage * pageHeight
}
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
scrollingFinished()
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollingFinished()
}
}
================================================
FILE: Sources/LazyPager/ViewDataProvider.swift
================================================
//
// ViewDataProvider.swift
//
//
// Created by Brian Floersch on 7/8/23.
//
import Foundation
import SwiftUI
import UIKit
public class ViewDataProvider<Content: View, DataCollecton: RandomAccessCollection, Element>: UIViewController, ViewLoader where DataCollecton.Index == Int, DataCollecton.Element == Element {
var viewLoader: (Element) -> Content
var data: DataCollecton
var config: Config<Element>
var pagerView: PagerView<Element, ViewDataProvider, Content>
var dataCount: Int {
return data.count
}
init(data: DataCollecton,
page: Binding<Int>,
config: Config<Element>,
viewLoader: @escaping (Element) -> Content) {
self.data = data
self.viewLoader = viewLoader
self.config = config
self.pagerView = PagerView(page: page, config: config)
super.init(nibName: nil, bundle: nil)
self.pagerView.viewLoader = self
pagerView.computeViewState()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func goToPage(_ page: Int, animated: Bool) {
pagerView.goToPage(page, animated: animated)
}
func reloadViews() {
pagerView.reloadViews()
pagerView.computeViewState()
}
// MARK: ViewLoader
func loadView(at index: Int) -> ZoomableView<Element, Content>? {
guard let dta = data[safe: index] else { return nil }
let hostingController = UIHostingController(rootView: viewLoader(dta))
return ZoomableView(hostingController: hostingController, index: index, data: dta, config: config)
}
func updateHostedView(for zoomableView: ZoomableView<Element, Content>) {
guard let dta = data[safe: zoomableView.index] else { return }
zoomableView.hostingController.rootView = viewLoader(dta)
}
// MARK: UIViewController
public override func loadView() {
self.view = pagerView
}
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
pagerView.isRotating = true
self.pagerView.scrollingFinished()
coordinator.animate(alongsideTransition: { context in }, completion: { context in
self.pagerView.isRotating = false
DispatchQueue.main.async {
self.pagerView.goToPage(self.pagerView.currentIndex, animated: false)
}
})
}
}
================================================
FILE: Sources/LazyPager/ZoomableView.swift
================================================
//
// ZoomableView.swift
// LazyPager
//
// Created by Brian Floersch on 7/4/23.
//
import Foundation
import UIKit
import SwiftUI
class ZoomableView<Element, Content: View>: UIScrollView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
var trailingConstraint: NSLayoutConstraint?
var leadingConstraint: NSLayoutConstraint?
var topConstraint: NSLayoutConstraint?
var bottomConstraint: NSLayoutConstraint?
var contentTopToContent: NSLayoutConstraint!
var contentTopToFrame: NSLayoutConstraint!
var contentBottomToFrame: NSLayoutConstraint!
var contentBottomToView: NSLayoutConstraint!
var config: Config<Element>
var bottomView: UIView
var allowScroll: Bool = true {
didSet {
if allowScroll, config.direction == .horizontal {
contentTopToFrame.isActive = false
contentBottomToFrame.isActive = false
bottomView.isHidden = false
contentTopToContent.isActive = true
contentBottomToView.isActive = true
} else {
contentTopToContent.isActive = false
contentBottomToView.isActive = false
contentTopToFrame.isActive = true
contentBottomToFrame.isActive = true
bottomView.isHidden = true
}
}
}
var wasTracking = false
var isZoomHappening = false
var dismissEnabled = false // Contorlled by PagerView to prevent flicker
var lastInset: CGFloat = 0
var currentZoomInsetAnimation: UIViewPropertyAnimator?
var hostingController: UIHostingController<Content>
var index: Int
var data: Element
var doubleTap: DoubleTap?
var lastBoundsSize: CGSize?
var view: UIView {
return hostingController.view
}
init(hostingController: UIHostingController<Content>, index: Int, data: Element, config: Config<Element>) {
self.index = index
self.hostingController = hostingController
self.data = data
self.config = config
let v = UIView()
self.bottomView = v
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
delegate = self
panGestureRecognizer.delegate = self
updateZoomConfig()
bouncesZoom = true
backgroundColor = .clear
alwaysBounceVertical = false
contentInsetAdjustmentBehavior = .never
if config.dismissCallback != nil {
alwaysBounceVertical = true
}
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
decelerationRate = .fast
// DEBUG
// backgroundColor = .red
addSubview(view)
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: leadingAnchor),
view.trailingAnchor.constraint(equalTo: trailingAnchor),
view.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor),
view.heightAnchor.constraint(equalTo: frameLayoutGuide.heightAnchor),
])
contentTopToFrame = view.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor)
contentTopToContent = view.topAnchor.constraint(equalTo: topAnchor)
contentBottomToFrame = view.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor)
contentBottomToView = view.bottomAnchor.constraint(equalTo: bottomView.topAnchor)
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
// This is for future support of a drawer view
let constant: CGFloat = config.dismissCallback == nil ? 0 : 1
NSLayoutConstraint.activate([
v.bottomAnchor.constraint(equalTo: bottomAnchor),
v.leadingAnchor.constraint(equalTo: frameLayoutGuide.leadingAnchor),
v.trailingAnchor.constraint(equalTo: frameLayoutGuide.trailingAnchor),
v.heightAnchor.constraint(equalToConstant: constant)
])
var singleTapGesture: UITapGestureRecognizer?
if config.tapCallback != nil {
let gesture = UITapGestureRecognizer(target: self, action: #selector(singleTap(_:)))
gesture.numberOfTapsRequired = 1
gesture.numberOfTouchesRequired = 1
addGestureRecognizer(gesture)
singleTapGesture = gesture
}
func setupDoubleTapGesture() {
let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(onDoubleTap(_:)))
doubleTapRecognizer.numberOfTapsRequired = 2
doubleTapRecognizer.numberOfTouchesRequired = 1
addGestureRecognizer(doubleTapRecognizer)
singleTapGesture?.require(toFail: doubleTapRecognizer)
}
if case .scale = doubleTap {
setupDoubleTapGesture()
} else if config.doubleTapCallback != nil {
setupDoubleTapGesture()
}
DispatchQueue.main.async {
self.updateState()
}
}
required init?(coder: NSCoder) {
fatalError("Not implemented")
}
func updateZoomConfig() {
switch config.zoomConfigGetter(data) {
case .disabled:
maximumZoomScale = 1
minimumZoomScale = 1
doubleTap = nil
case let .custom(min, max, doubleTap):
minimumZoomScale = min
maximumZoomScale = max
self.doubleTap = doubleTap
}
}
@objc func singleTap(_ recognizer: UITapGestureRecognizer) {
config.tapCallback?()
}
@objc func onDoubleTap(_ recognizer: UITapGestureRecognizer) {
config.doubleTapCallback?()
if case let .scale(scale) = doubleTap {
let pointInView = recognizer.location(in: view)
zoom(at: pointInView, scale: scale)
updateInsets()
}
}
func updateState() {
updateZoomConfig()
allowScroll = zoomScale == 1
if contentOffset.y > config.pinchGestureEnableOffset, allowScroll {
pinchGestureRecognizer?.isEnabled = false
} else {
pinchGestureRecognizer?.isEnabled = true
}
if allowScroll {
if dismissEnabled, config.dismissCallback != nil {
let offset = contentOffset.y
if offset < 0 {
let absoluteDragOffset = normalize(from: 0, at: abs(offset), to: frame.size.height)
let fadeOffset = normalize(from: 0, at: absoluteDragOffset, to: config.fullFadeOnDragAt)
config.backgroundOpacity?.wrappedValue = 1 - fadeOffset
} else {
DispatchQueue.main.async {
self.config.backgroundOpacity?.wrappedValue = 1
}
}
}
wasTracking = isTracking
}
}
func zoom(at point: CGPoint, scale: CGFloat) {
let mid = lerp(from: minimumZoomScale, to: maximumZoomScale, by: scale)
let newZoomScale = zoomScale == minimumZoomScale ? mid : minimumZoomScale
let size = bounds.size
let w = size.width / newZoomScale
let h = size.height / newZoomScale
let x = point.x - (w * 0.5)
let y = point.y - (h * 0.5)
zoom(to: CGRect(x: x, y: y, width: w, height: h), animated: true)
}
override func layoutSubviews() {
super.layoutSubviews()
// Ensures insets are updated when the screen rotates
if bounds.size != lastBoundsSize {
lastBoundsSize = bounds.size
updateInsets()
}
}
// MARK: UIScrollViewDelegate methods
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
isZoomHappening = true
updateState()
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
isZoomHappening = false
updateState()
updateInsets()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateState()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return view
}
func updateInsets() {
let w: CGFloat = view.intrinsicContentSize.width * UIScreen.main.scale
let h: CGFloat = view.intrinsicContentSize.height * UIScreen.main.scale
let ratioW = view.frame.width / w
let ratioH = view.frame.height / h
let ratio = ratioW < ratioH ? ratioW : ratioH
let newWidth = w*ratio
let newHeight = h*ratio
let left = 0.5 * (newWidth * zoomScale > view.frame.width
? (newWidth - view.frame.width)
: (frame.width - view.frame.width))
let top = 0.5 * (newHeight * zoomScale > view.frame.height
? (newHeight - view.frame.height)
: (frame.height - view.frame.height))
if zoomScale <= maximumZoomScale {
let targetInsets = UIEdgeInsets(
top: top,
left: left,
bottom: top,
right: left
)
UIView.animate(withDuration: 0.3) {
self.contentInset = targetInsets
}
}
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let scrollViewSize = scrollView.bounds.size
let zoomViewSize = view.frame.size
let horizontalInset = max(0, (scrollViewSize.width - zoomViewSize.width) / 2)
let verticalInset = max(0, (scrollViewSize.height - zoomViewSize.height) / 2)
scrollView.contentInset = UIEdgeInsets(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
config.onZoomHandler?(data, scrollView.zoomScale)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let percentage = contentOffset.y / (contentSize.height - bounds.size.height)
if wasTracking,
percentage < -config.dismissTriggerOffset,
!isZoomHappening,
velocity.y < -config.dismissVelocity,
config.dismissCallback != nil {
dismissEnabled = false // prevent touch interaction from messing with animation of opacity.
let ogFram = frame.origin
withAnimation(.linear(duration: self.config.dismissAnimationLength)) {
self.config.backgroundOpacity?.wrappedValue = 0
}
frame.origin.y = -contentOffset.y
UIView.animate(withDuration: self.config.dismissAnimationLength, animations: {
self.frame.origin = CGPoint(x: ogFram.x, y: self.frame.size.height)
}) { _ in
if self.config.shouldCancelSwiftUIAnimationsOnDismiss {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
self.config.dismissCallback?()
}
} else {
self.config.dismissCallback?()
}
}
}
}
// MARK: UIGestureRecognizerDelegate
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// We only want to intercept our own pan gesture.
guard gestureRecognizer == self.panGestureRecognizer else {
return true
}
let panGesture = self.panGestureRecognizer
let velocity = panGesture.velocity(in: self)
// This logic is for the horizontal pager.
if config.direction == .horizontal {
// If the swipe is mostly vertical, it's for dismissal. Let it happen.
if abs(velocity.y) > abs(velocity.x) {
return true
}
// It's a horizontal swipe. Should we let our own pan gesture begin?
// If not zoomed, NO. The pager should handle all horizontal movement.
if zoomScale <= minimumZoomScale {
return false // Prevent our pan, let PagerView handle it.
}
// If we ARE zoomed, check if we're at the horizontal edges.
let maxOffsetX = contentSize.width - bounds.width + contentInset.right
let minOffsetX = -contentInset.left
let isAtRightEdge = contentOffset.x >= maxOffsetX - 1.0
let isAtLeftEdge = contentOffset.x <= minOffsetX + 1.0
// At the right edge and trying to swipe left (to the next page).
if isAtRightEdge && velocity.x < 0 {
return false // Prevent our pan, let PagerView handle it.
}
// At the left edge and trying to swipe right (to the previous page).
if isAtLeftEdge && velocity.x > 0 {
return false // Prevent our pan, let PagerView handle it.
}
} else { // Vertical Pager
// If the swipe is mostly horizontal, let it happen.
if abs(velocity.x) > abs(velocity.y) {
return true
}
// It's a vertical swipe. Should we let our own pan gesture begin?
// If not zoomed, NO. The pager should handle all vertical movement.
if zoomScale <= minimumZoomScale {
return false // Prevent our pan, let PagerView handle it.
}
// If we ARE zoomed, check if we're at the vertical edges.
let maxOffsetY = contentSize.height - bounds.height + contentInset.bottom
let minOffsetY = -contentInset.top
let isAtBottomEdge = contentOffset.y >= maxOffsetY - 1.0
let isAtTopEdge = contentOffset.y <= minOffsetY + 1.0
// At the bottom edge and trying to swipe up (to the next page).
if isAtBottomEdge && velocity.y < 0 {
return false // Prevent our pan, let PagerView handle it.
}
// At the top edge and trying to swipe down (to the previous page).
if isAtTopEdge && velocity.y > 0 {
return false // Prevent our pan, let PagerView handle it.
}
}
// If we're not at an edge while zoomed, our pan gesture should begin.
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// We no longer need simultaneous recognition. This simplifies the logic and
// prevents the differential panning issues.
return false
}
}
================================================
FILE: Tests/LazyPagerTests/LazyPagerTests.swift
================================================
import XCTest
@testable import LazyPager
final class LazyPagerTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
// XCTAssertEqual(LazyPager().text, "Hello, World!")
}
}
gitextract_8knwvjye/
├── .github/
│ └── ISSUE_TEMPLATE/
│ └── bug_report.md
├── .gitignore
├── Examples/
│ ├── LazyPagerExample.xcodeproj/
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ ├── LazyPagerExampleApp/
│ │ ├── AnimatedPagerControlsExample.swift
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── nora1.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora2.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora3.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora4.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── nora5.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── nora6.imageset/
│ │ │ └── Contents.json
│ │ ├── EnvironmentExample.swift
│ │ ├── FullTestView.swift
│ │ ├── InsetTest.swift
│ │ ├── LazyPagerExampleApp.swift
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ ├── SimpleExample.swift
│ │ └── VerticalMediaPager.swift
│ ├── LazyPagerExampleAppTests/
│ │ └── LazyPagerExampleAppTests.swift
│ └── LazyPagerExampleAppUITests/
│ ├── ImageScrollViewUITests.swift
│ └── ImageScrollViewUITestsLaunchTests.swift
├── LICENSE
├── Package.swift
├── README.md
├── Sources/
│ └── LazyPager/
│ ├── ClearFullScreenBackground.swift
│ ├── Collection+Extensions.swift
│ ├── LazyPager.swift
│ ├── Math.swift
│ ├── PagerView.swift
│ ├── ViewDataProvider.swift
│ └── ZoomableView.swift
└── Tests/
└── LazyPagerTests/
└── LazyPagerTests.swift
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (111K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 544,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".gitignore",
"chars": 165,
"preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\nDerivedData/\n.swiftpm/config/registries.json\n.swiftpm/xcode/package"
},
{
"path": "Examples/LazyPagerExample.xcodeproj/project.pbxproj",
"chars": 27203,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 60;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Examples/LazyPagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 197,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:/Users/brian/de"
},
{
"path": "Examples/LazyPagerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Examples/LazyPagerExampleApp/AnimatedPagerControlsExample.swift",
"chars": 1504,
"preview": "import SwiftUI\nimport LazyPager\n\n\nstruct AnimatedPagerControlsExample: View {\n \n @State var data = [\n \"nora"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json",
"chars": 123,
"preview": "{\n \"colors\" : [\n {\n \"idiom\" : \"universal\"\n }\n ],\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 177,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"universal\",\n \"platform\" : \"ios\",\n \"size\" : \"1024x1024\"\n }\n ],\n \"i"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora1.imageset/Contents.json",
"chars": 345,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"356181627_737281678149026_5519646735590788375_n.jpg\",\n \"idiom\" : \"univer"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora2.imageset/Contents.json",
"chars": 344,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"356184881_810974757010572_166165563303848404_n.jpg\",\n \"idiom\" : \"univers"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora3.imageset/Contents.json",
"chars": 346,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"356184996_1504506290292039_6439519590743317419_n.jpg\",\n \"idiom\" : \"unive"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora4.imageset/Contents.json",
"chars": 345,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"356187567_797832131883666_8693445044613773171_n.jpg\",\n \"idiom\" : \"univer"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora5.imageset/Contents.json",
"chars": 345,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"356198313_803291668248047_1588179413198578920_n.jpg\",\n \"idiom\" : \"univer"
},
{
"path": "Examples/LazyPagerExampleApp/Assets.xcassets/nora6.imageset/Contents.json",
"chars": 345,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"358743821_933760767702238_5920729387861732707_n.jpg\",\n \"idiom\" : \"univer"
},
{
"path": "Examples/LazyPagerExampleApp/EnvironmentExample.swift",
"chars": 1520,
"preview": "//\n// EnvironmentExample.swift\n// LazyPagerExample\n//\n// Created by Brian Floersch on 4/26/25.\n//\n\nimport SwiftUI\nimp"
},
{
"path": "Examples/LazyPagerExampleApp/FullTestView.swift",
"chars": 3735,
"preview": "//\n// ContentView.swift\n// LazyPager\n//\n// Created by Brian Floersch on 7/2/23.\n//\n\nimport SwiftUI\nimport LazyPager\n\n"
},
{
"path": "Examples/LazyPagerExampleApp/InsetTest.swift",
"chars": 1672,
"preview": "import SwiftUI\nimport LazyPager\n\n\nstruct InsetTest: View {\n \n @State var data = [\n \"nora1\",\n \"nora2\""
},
{
"path": "Examples/LazyPagerExampleApp/LazyPagerExampleApp.swift",
"chars": 1697,
"preview": "//\n// LazyPagerApp.swift\n// LazyPager\n//\n// Created by Brian Floersch on 7/2/23.\n//\n\nimport SwiftUI\n\n@main\nstruct Laz"
},
{
"path": "Examples/LazyPagerExampleApp/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Examples/LazyPagerExampleApp/SimpleExample.swift",
"chars": 579,
"preview": "import SwiftUI\nimport LazyPager\n\n\nstruct SimpleExample: View {\n \n @State var data = [\n \"nora1\",\n \"no"
},
{
"path": "Examples/LazyPagerExampleApp/VerticalMediaPager.swift",
"chars": 2349,
"preview": "import SwiftUI\nimport LazyPager\n\n\nstruct VerticalMediaPager: View {\n \n @State var data = [\n \"nora1\",\n "
},
{
"path": "Examples/LazyPagerExampleAppTests/LazyPagerExampleAppTests.swift",
"chars": 1229,
"preview": "//\n// LazyPagerTests.swift\n// LazyPagerTests\n//\n// Created by Brian Floersch on 7/2/23.\n//\n\nimport XCTest\n@testable i"
},
{
"path": "Examples/LazyPagerExampleAppUITests/ImageScrollViewUITests.swift",
"chars": 1379,
"preview": "//\n// LazyPagerUITests.swift\n// LazyPagerUITests\n//\n// Created by Brian Floersch on 7/2/23.\n//\n\nimport XCTest\n\nfinal "
},
{
"path": "Examples/LazyPagerExampleAppUITests/ImageScrollViewUITestsLaunchTests.swift",
"chars": 809,
"preview": "//\n// LazyPagerUITestsLaunchTests.swift\n// LazyPagerUITests\n//\n// Created by Brian Floersch on 7/2/23.\n//\n\nimport XCT"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2022 Brian Floersch\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "Package.swift",
"chars": 1066,
"preview": "// swift-tools-version: 5.7\n// The swift-tools-version declares the minimum version of Swift required to build this pack"
},
{
"path": "README.md",
"chars": 6943,
"preview": "# LazyPager for SwiftUI\n\nA buttery smooth, lazy loaded, panning, zooming, and gesture dismissible view pager view for Sw"
},
{
"path": "Sources/LazyPager/ClearFullScreenBackground.swift",
"chars": 525,
"preview": "//\n// ClearFullScreenBackground.swift\n// \n//\n// Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\nimport Swi"
},
{
"path": "Sources/LazyPager/Collection+Extensions.swift",
"chars": 335,
"preview": "//\n// Collection+Extensons.swift\n// \n//\n// Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\n\nextension Coll"
},
{
"path": "Sources/LazyPager/LazyPager.swift",
"chars": 7971,
"preview": "//\n// LazyPager.swift\n// LazyPager\n//\n// Created by Brian Floersch on 7/6/23.\n//\n\nimport Foundation\nimport UIKit\nimpo"
},
{
"path": "Sources/LazyPager/Math.swift",
"chars": 353,
"preview": "//\n// Math.swift\n// \n//\n// Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\n\nfunc lerp(from: CGFloat, to: C"
},
{
"path": "Sources/LazyPager/PagerView.swift",
"chars": 18465,
"preview": "//\n// PagerView.swift\n// \n//\n// Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\nimport UIKit\nimport SwiftU"
},
{
"path": "Sources/LazyPager/ViewDataProvider.swift",
"chars": 2619,
"preview": "//\n// ViewDataProvider.swift\n// \n//\n// Created by Brian Floersch on 7/8/23.\n//\n\nimport Foundation\nimport SwiftUI\nimpo"
},
{
"path": "Sources/LazyPager/ZoomableView.swift",
"chars": 15217,
"preview": "//\n// ZoomableView.swift\n// LazyPager\n//\n// Created by Brian Floersch on 7/4/23.\n//\n\nimport Foundation\nimport UIKit\ni"
},
{
"path": "Tests/LazyPagerTests/LazyPagerTests.swift",
"chars": 348,
"preview": "import XCTest\n@testable import LazyPager\n\nfinal class LazyPagerTests: XCTestCase {\n func testExample() throws {\n "
}
]
About this extraction
This page contains the full source code of the gh123man/SwiftUI-LazyPager GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (99.8 KB), approximately 26.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.