app框架搭建,播放器开发

This commit is contained in:
曾觉新 2025-04-09 18:24:58 +08:00
parent 230f99a2dc
commit 335ede5be7
104 changed files with 7202 additions and 0 deletions

38
Podfile Normal file
View File

@ -0,0 +1,38 @@
# Uncomment the next line to define a global platform for your project
platform :ios, '13.0'
#source 'https://github.com/CocoaPods/Specs'
source 'https://cdn.cocoapods.org/'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
end
end
end
target 'ShortPlay' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'Moya' #网络框架
pod 'SnapKit' #布局
pod 'SmartCodable' #数据解析
pod 'YYKit' #工具类
pod 'MJRefresh' #刷新控件
pod 'Toast' #吐司提示
pod 'ZFPlayer/AVPlayer' #播放器
pod 'KTVHTTPCache' #视频缓存
target 'ShortPlayTests' do
inherit! :search_paths
# Pods for testing
end
target 'ShortPlayUITests' do
# Pods for testing
end
end

58
Podfile.lock Normal file
View File

@ -0,0 +1,58 @@
PODS:
- Alamofire (5.10.2)
- CocoaAsyncSocket (7.6.5)
- KTVHTTPCache (3.0.2):
- CocoaAsyncSocket
- MJRefresh (3.7.9)
- Moya (15.0.0):
- Moya/Core (= 15.0.0)
- Moya/Core (15.0.0):
- Alamofire (~> 5.0)
- SmartCodable (4.3.2)
- SnapKit (5.7.1)
- Toast (4.1.1)
- YYKit (1.0.9):
- YYKit/no-arc (= 1.0.9)
- YYKit/no-arc (1.0.9)
- ZFPlayer/AVPlayer (4.1.4):
- ZFPlayer/Core
- ZFPlayer/Core (4.1.4)
DEPENDENCIES:
- KTVHTTPCache
- MJRefresh
- Moya
- SmartCodable
- SnapKit
- Toast
- YYKit
- ZFPlayer/AVPlayer
SPEC REPOS:
trunk:
- Alamofire
- CocoaAsyncSocket
- KTVHTTPCache
- MJRefresh
- Moya
- SmartCodable
- SnapKit
- Toast
- YYKit
- ZFPlayer
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
KTVHTTPCache: 5711692cdf9a5ecfe829b1e16577deb3ffe3dc86
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
Moya: 138f0573e53411fb3dc17016add0b748dfbd78ee
SmartCodable: 88fbf3d65207c2376fdbce4b080a3d578cb51be8
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
YYKit: 7cda43304a8dc3696c449041e2cb3107b4e236e7
ZFPlayer: 5cf39e8d9f0c2394a014b0db4767b5b5a6bffe13
PODFILE CHECKSUM: 422706ab4a12e286d2106acff478496d948912e8
COCOAPODS: 1.16.2

View File

@ -0,0 +1,761 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
1DBC41082DA4FC140093FCB0 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 1DBC41072DA4FC140093FCB0 /* Kingfisher */; };
44750E76352CA5F5653C130B /* Pods_ShortPlay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CB082E16E94AEEE7602DC87 /* Pods_ShortPlay.framework */; };
8259C195B8627F99A0DA2633 /* Pods_ShortPlayTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3049C811FA3662418C9EEACE /* Pods_ShortPlayTests.framework */; };
F30A67DA1559E80D2303D8BD /* Pods_ShortPlay_ShortPlayUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DB457634938350C07D587D4 /* Pods_ShortPlay_ShortPlayUITests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
1DBC40702DA4EE010093FCB0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 1DBC40512DA4EDFC0093FCB0 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1DBC40582DA4EDFC0093FCB0;
remoteInfo = ShortPlay;
};
1DBC407A2DA4EE010093FCB0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 1DBC40512DA4EDFC0093FCB0 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1DBC40582DA4EDFC0093FCB0;
remoteInfo = ShortPlay;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
1AC17585C566B70F526D3938 /* Pods-ShortPlay.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay.debug.xcconfig"; path = "Target Support Files/Pods-ShortPlay/Pods-ShortPlay.debug.xcconfig"; sourceTree = "<group>"; };
1B957067B5FAB12084843406 /* Pods-ShortPlay-ShortPlayUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay-ShortPlayUITests.release.xcconfig"; path = "Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests.release.xcconfig"; sourceTree = "<group>"; };
1DBC40592DA4EDFC0093FCB0 /* ShortPlay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShortPlay.app; sourceTree = BUILT_PRODUCTS_DIR; };
1DBC406F2DA4EE010093FCB0 /* ShortPlayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShortPlayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
1DBC40792DA4EE010093FCB0 /* ShortPlayUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShortPlayUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
27D1AF89FD4CC591EFA405F2 /* Pods-ShortPlay-ShortPlayUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay-ShortPlayUITests.debug.xcconfig"; path = "Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests.debug.xcconfig"; sourceTree = "<group>"; };
3049C811FA3662418C9EEACE /* Pods_ShortPlayTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShortPlayTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3CB082E16E94AEEE7602DC87 /* Pods_ShortPlay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShortPlay.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7AA7065C6971326630D3239D /* Pods-ShortPlayTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlayTests.release.xcconfig"; path = "Target Support Files/Pods-ShortPlayTests/Pods-ShortPlayTests.release.xcconfig"; sourceTree = "<group>"; };
8DB457634938350C07D587D4 /* Pods_ShortPlay_ShortPlayUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShortPlay_ShortPlayUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9DF7FB1047F612A65429345F /* Pods-ShortPlay.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay.release.xcconfig"; path = "Target Support Files/Pods-ShortPlay/Pods-ShortPlay.release.xcconfig"; sourceTree = "<group>"; };
9E1BF460A8DF01704AD3DB63 /* Pods-ShortPlayTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlayTests.debug.xcconfig"; path = "Target Support Files/Pods-ShortPlayTests/Pods-ShortPlayTests.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
1DBC41052DA4F98D0093FCB0 /* Exceptions for "ShortPlay" folder in "ShortPlay" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Source/Info.plist,
);
target = 1DBC40582DA4EDFC0093FCB0 /* ShortPlay */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
1DBC40722DA4EE010093FCB0 /* ShortPlayTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = ShortPlayTests;
sourceTree = "<group>";
};
1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = ShortPlayUITests;
sourceTree = "<group>";
};
1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
1DBC41052DA4F98D0093FCB0 /* Exceptions for "ShortPlay" folder in "ShortPlay" target */,
);
path = ShortPlay;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
1DBC40562DA4EDFC0093FCB0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1DBC41082DA4FC140093FCB0 /* Kingfisher in Frameworks */,
44750E76352CA5F5653C130B /* Pods_ShortPlay.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1DBC406C2DA4EE010093FCB0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8259C195B8627F99A0DA2633 /* Pods_ShortPlayTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
1DBC40762DA4EE010093FCB0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F30A67DA1559E80D2303D8BD /* Pods_ShortPlay_ShortPlayUITests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0061C3496D158807460301A9 /* Pods */ = {
isa = PBXGroup;
children = (
1AC17585C566B70F526D3938 /* Pods-ShortPlay.debug.xcconfig */,
9DF7FB1047F612A65429345F /* Pods-ShortPlay.release.xcconfig */,
27D1AF89FD4CC591EFA405F2 /* Pods-ShortPlay-ShortPlayUITests.debug.xcconfig */,
1B957067B5FAB12084843406 /* Pods-ShortPlay-ShortPlayUITests.release.xcconfig */,
9E1BF460A8DF01704AD3DB63 /* Pods-ShortPlayTests.debug.xcconfig */,
7AA7065C6971326630D3239D /* Pods-ShortPlayTests.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
1DBC40502DA4EDFC0093FCB0 = {
isa = PBXGroup;
children = (
1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */,
1DBC40722DA4EE010093FCB0 /* ShortPlayTests */,
1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */,
1DBC405A2DA4EDFC0093FCB0 /* Products */,
0061C3496D158807460301A9 /* Pods */,
B6C9E282BAC4C4B3E926A853 /* Frameworks */,
);
sourceTree = "<group>";
};
1DBC405A2DA4EDFC0093FCB0 /* Products */ = {
isa = PBXGroup;
children = (
1DBC40592DA4EDFC0093FCB0 /* ShortPlay.app */,
1DBC406F2DA4EE010093FCB0 /* ShortPlayTests.xctest */,
1DBC40792DA4EE010093FCB0 /* ShortPlayUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
B6C9E282BAC4C4B3E926A853 /* Frameworks */ = {
isa = PBXGroup;
children = (
3CB082E16E94AEEE7602DC87 /* Pods_ShortPlay.framework */,
8DB457634938350C07D587D4 /* Pods_ShortPlay_ShortPlayUITests.framework */,
3049C811FA3662418C9EEACE /* Pods_ShortPlayTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
1DBC40582DA4EDFC0093FCB0 /* ShortPlay */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1DBC40822DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlay" */;
buildPhases = (
801A3E3FF53193556BBE9EBF /* [CP] Check Pods Manifest.lock */,
1DBC40552DA4EDFC0093FCB0 /* Sources */,
1DBC40562DA4EDFC0093FCB0 /* Frameworks */,
1DBC40572DA4EDFC0093FCB0 /* Resources */,
99BF4E2B3615B1F54D05DA28 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */,
);
name = ShortPlay;
productName = ShortPlay;
productReference = 1DBC40592DA4EDFC0093FCB0 /* ShortPlay.app */;
productType = "com.apple.product-type.application";
};
1DBC406E2DA4EE010093FCB0 /* ShortPlayTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1DBC40872DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayTests" */;
buildPhases = (
DD54CED5D8E8015BF138DA32 /* [CP] Check Pods Manifest.lock */,
1DBC406B2DA4EE010093FCB0 /* Sources */,
1DBC406C2DA4EE010093FCB0 /* Frameworks */,
1DBC406D2DA4EE010093FCB0 /* Resources */,
);
buildRules = (
);
dependencies = (
1DBC40712DA4EE010093FCB0 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
1DBC40722DA4EE010093FCB0 /* ShortPlayTests */,
);
name = ShortPlayTests;
productName = ShortPlayTests;
productReference = 1DBC406F2DA4EE010093FCB0 /* ShortPlayTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
1DBC40782DA4EE010093FCB0 /* ShortPlayUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1DBC408A2DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayUITests" */;
buildPhases = (
7A20F9056D18DF2605250323 /* [CP] Check Pods Manifest.lock */,
1DBC40752DA4EE010093FCB0 /* Sources */,
1DBC40762DA4EE010093FCB0 /* Frameworks */,
1DBC40772DA4EE010093FCB0 /* Resources */,
36F6362AEACE4C584F0D7341 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
1DBC407B2DA4EE010093FCB0 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */,
);
name = ShortPlayUITests;
productName = ShortPlayUITests;
productReference = 1DBC40792DA4EE010093FCB0 /* ShortPlayUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
1DBC40512DA4EDFC0093FCB0 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620;
TargetAttributes = {
1DBC40582DA4EDFC0093FCB0 = {
CreatedOnToolsVersion = 16.2;
};
1DBC406E2DA4EE010093FCB0 = {
CreatedOnToolsVersion = 16.2;
TestTargetID = 1DBC40582DA4EDFC0093FCB0;
};
1DBC40782DA4EE010093FCB0 = {
CreatedOnToolsVersion = 16.2;
TestTargetID = 1DBC40582DA4EDFC0093FCB0;
};
};
};
buildConfigurationList = 1DBC40542DA4EDFC0093FCB0 /* Build configuration list for PBXProject "ShortPlay" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 1DBC40502DA4EDFC0093FCB0;
minimizedProjectReferenceProxies = 1;
packageReferences = (
1DBC41062DA4FC140093FCB0 /* XCRemoteSwiftPackageReference "Kingfisher" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 1DBC405A2DA4EDFC0093FCB0 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
1DBC40582DA4EDFC0093FCB0 /* ShortPlay */,
1DBC406E2DA4EE010093FCB0 /* ShortPlayTests */,
1DBC40782DA4EE010093FCB0 /* ShortPlayUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
1DBC40572DA4EDFC0093FCB0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
1DBC406D2DA4EE010093FCB0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
1DBC40772DA4EE010093FCB0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
36F6362AEACE4C584F0D7341 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
7A20F9056D18DF2605250323 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ShortPlay-ShortPlayUITests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
801A3E3FF53193556BBE9EBF /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ShortPlay-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
99BF4E2B3615B1F54D05DA28 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay/Pods-ShortPlay-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay/Pods-ShortPlay-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ShortPlay/Pods-ShortPlay-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
DD54CED5D8E8015BF138DA32 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ShortPlayTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
1DBC40552DA4EDFC0093FCB0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
1DBC406B2DA4EE010093FCB0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
1DBC40752DA4EE010093FCB0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
1DBC40712DA4EE010093FCB0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 1DBC40582DA4EDFC0093FCB0 /* ShortPlay */;
targetProxy = 1DBC40702DA4EE010093FCB0 /* PBXContainerItemProxy */;
};
1DBC407B2DA4EE010093FCB0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 1DBC40582DA4EDFC0093FCB0 /* ShortPlay */;
targetProxy = 1DBC407A2DA4EE010093FCB0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
1DBC40832DA4EE010093FCB0 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1AC17585C566B70F526D3938 /* Pods-ShortPlay.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 36QJHAN62Q;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShortPlay/Source/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = "";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlay;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "ShortPlay/Source/ShortPlay-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
1DBC40842DA4EE010093FCB0 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9DF7FB1047F612A65429345F /* Pods-ShortPlay.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 36QJHAN62Q;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShortPlay/Source/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = "";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlay;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "ShortPlay/Source/ShortPlay-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
1DBC40852DA4EE010093FCB0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
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;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
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 = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
1DBC40862DA4EE010093FCB0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
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;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
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 = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
1DBC40882DA4EE010093FCB0 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9E1BF460A8DF01704AD3DB63 /* Pods-ShortPlayTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 36QJHAN62Q;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ShortPlay.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ShortPlay";
};
name = Debug;
};
1DBC40892DA4EE010093FCB0 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AA7065C6971326630D3239D /* Pods-ShortPlayTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 36QJHAN62Q;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ShortPlay.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ShortPlay";
};
name = Release;
};
1DBC408B2DA4EE010093FCB0 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 27D1AF89FD4CC591EFA405F2 /* Pods-ShortPlay-ShortPlayUITests.debug.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 36QJHAN62Q;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = ShortPlay;
};
name = Debug;
};
1DBC408C2DA4EE010093FCB0 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1B957067B5FAB12084843406 /* Pods-ShortPlay-ShortPlayUITests.release.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 36QJHAN62Q;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = ShortPlay;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
1DBC40542DA4EDFC0093FCB0 /* Build configuration list for PBXProject "ShortPlay" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1DBC40852DA4EE010093FCB0 /* Debug */,
1DBC40862DA4EE010093FCB0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1DBC40822DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlay" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1DBC40832DA4EE010093FCB0 /* Debug */,
1DBC40842DA4EE010093FCB0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1DBC40872DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1DBC40882DA4EE010093FCB0 /* Debug */,
1DBC40892DA4EE010093FCB0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1DBC408A2DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1DBC408B2DA4EE010093FCB0 /* Debug */,
1DBC408C2DA4EE010093FCB0 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
1DBC41062DA4FC140093FCB0 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 8.3.1;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
1DBC41072DA4FC140093FCB0 /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 1DBC41062DA4FC140093FCB0 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 1DBC40512DA4EDFC0093FCB0 /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:ShortPlay.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,15 @@
{
"originHash" : "917a2b512148612347b307b5f8d624e74df48af36fa322c80901a86fc0e1317f",
"pins" : [
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher.git",
"state" : {
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
"version" : "8.3.1"
}
}
],
"version" : 3
}

View File

@ -0,0 +1,73 @@
//
// AppDelegate+Config.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
extension AppDelegate {
func appConfig() {
// UIView.et_Awake()
tabBarConfig()
// keyBoardStyle()
}
}
extension AppDelegate {
///tabBar
private func tabBarConfig() {
let tabBar = UITabBar.appearance();
let font = UIFont.fontRegular(ofSize: 10)
let normalColor = UIColor.color7F7F80()
let selectedColor = UIColor.colorFF0089()
let backgroundImage = UIImage(color: .clear)
let backgroundColor = UIColor.clear
let shadowImage = UIImage()
let shadowColor = UIColor.clear
let par = NSMutableParagraphStyle()
par.alignment = .center;
let normalAttributes = [NSAttributedString.Key.foregroundColor : normalColor,
NSAttributedString.Key.paragraphStyle : par,
NSAttributedString.Key.font: font,
]
let selectedAttributes = [NSAttributedString.Key.foregroundColor:selectedColor,
NSAttributedString.Key.paragraphStyle : par,
NSAttributedString.Key.font: font,
]
let appearance = UITabBarAppearance()
let normal = appearance.stackedLayoutAppearance.normal
normal.titleTextAttributes = normalAttributes
let selected = appearance.stackedLayoutAppearance.selected
selected.titleTextAttributes = selectedAttributes
appearance.backgroundImage = backgroundImage;
appearance.backgroundColor = backgroundColor;
appearance.shadowImage = shadowImage
appearance.shadowColor = shadowColor
appearance.backgroundEffect = nil
//
// appearance.configureWithTransparentBackground()
tabBar.standardAppearance = appearance;
if #available(iOS 15.0, *) {
tabBar.scrollEdgeAppearance = appearance
}
}
}

View File

@ -0,0 +1,40 @@
//
// AppDelegate.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.appConfig()
SPLoginManager.manager.requestVisitorLogin(completer: nil)
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}

View File

@ -0,0 +1,53 @@
//
// SceneDelegate.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = SPTabBarController()
window?.makeKeyAndVisible()
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}

View File

@ -0,0 +1,35 @@
//
// SPNavigationController.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
if children.count > 0 {
viewController.hidesBottomBarWhenPushed = true
}
super.pushViewController(viewController, animated: animated)
}
override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
for (index, value) in viewControllers.enumerated() {
if index != 0 {
value.hidesBottomBarWhenPushed = true
}
}
super.setViewControllers(viewControllers, animated: animated)
}
}

View File

@ -0,0 +1,47 @@
//
// SPTabBarController.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let nav1 = createNavigationController(viewController: SPHomePageController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
let nav2 = createNavigationController(viewController: SPForYouViewController(), title: "For You".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
self.viewControllers = [nav1, nav2]
}
//MARK:-------------- --------------
override var childForStatusBarStyle: UIViewController? {
return self.selectedViewController
}
override var childForStatusBarHidden: UIViewController? {
return self.selectedViewController
}
}
extension SPTabBarController {
func createNavigationController(viewController: UIViewController, title: String?, image: UIImage?, selectedImage: UIImage?) -> UINavigationController {
let nav = SPNavigationController(rootViewController: viewController)
nav.tabBarItem.selectedImage = selectedImage
nav.tabBarItem.image = image
nav.tabBarItem.title = title
return nav
}
}

View File

@ -0,0 +1,155 @@
//
// SPViewController.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPViewController: UIViewController, JYPageChildContollerProtocol {
var statusBarStyle: UIStatusBarStyle? {
didSet {
self.setNeedsStatusBarAppearanceUpdate()
}
}
var statusBarHidden: Bool = false {
didSet {
// self.setNeedsStatusBarAppearanceUpdate()
}
}
private(set) var isViewDidLoad = false
private(set) var isDidAppear = false
override func viewDidLoad() {
super.viewDidLoad()
self.isViewDidLoad = true
self.edgesForExtendedLayout = []
if let navi = navigationController {
if navi.visibleViewController == self {
if navi.viewControllers.count > 1 {
configNavigationBack()
}
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
isDidAppear = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
isDidAppear = false
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
isDidAppear = false
}
//MARK:-------------- --------------
override var preferredStatusBarStyle: UIStatusBarStyle {
if let statusBarStyle = statusBarStyle {
return statusBarStyle
} else {
return .default
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
///
override var shouldAutorotate: Bool {
return false
}
///
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
///
func fetchChildControllerScrollView() -> UIScrollView? {
return nil
}
}
extension UIViewController {
func configNavigationBack(_ imageName: String = "left_arrow_icon_01") {
let image = UIImage(named: imageName)
let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleBack))
navigationItem.leftBarButtonItem = leftBarButtonItem
}
@objc func handleBack() {
self.sp_toLastViewController(animated: true)
}
}
extension UIViewController {
func sp_toLastViewController(animated:Bool)
{
if self.navigationController != nil
{
if self.navigationController?.viewControllers.count == 1
{
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
else if self.presentingViewController != nil {
self.dismiss(animated: animated, completion: nil)
}
}
}
extension UIViewController {
///
func setNavigationNormalStyle(backgroundColor: UIColor = UINavigationBar.sp_normalBackgroundColor,
isTranslucent: Bool = false,
prefersLargeTitles: Bool = false)
{
self.setNavigationBackgroundColor(color: backgroundColor, isTranslucent: isTranslucent)
self.setNavigationTitleStyle()
self.navigationController?.navigationBar.prefersLargeTitles = prefersLargeTitles
}
///
func setNavigationBackgroundColor(color: UIColor?, isTranslucent: Bool = false) {
guard let nav = navigationController else { return }
if nav.visibleViewController == self {
nav.navigationBar.sp_setBackgroundColor(backgroundColor: color)
nav.navigationBar.sp_setTranslucent(isTranslucent: isTranslucent)
}
}
///
func setNavigationTitleStyle(color: UIColor? = nil, font: UIFont? = nil) {
guard let nav = navigationController else { return }
if nav.visibleViewController == self {
//
var titleTextAttributes = UINavigationBar.sp_normalTitleTextAttributes
if let color = color {
titleTextAttributes[NSAttributedString.Key.foregroundColor] = color
}
if let font = font {
titleTextAttributes[NSAttributedString.Key.font] = font
}
nav.navigationBar.sp_setTitleTextAttributes(titleTextAttributes: titleTextAttributes)
}
}
}

View File

@ -0,0 +1,58 @@
//
// SPDefine.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
//MARK:-------------- --------------
let kSPScreenWidth = UIScreen.main.bounds.size.width
let kSPScreenHeight = UIScreen.main.bounds.size.height
let kSPNavBarHeight : CGFloat = ((kSPStatusbarHeight != 59) ? kSPStatusbarHeight: 54) + 44 //
let kSPTabBarHeight : CGFloat = kSPTabbarSafeBottomMargin + 49
///
let kSPStatusbarHeight: CGFloat = {
var top: CGFloat = 20
if #available(iOS 11.0, *) {
top = UIApplication.shared.windows[0].safeAreaInsets.top
top = (top != 59) ? top : 54 //
top = top == 0 ? 20 : top
}
return top
}()
///tab
let kSPTabbarSafeBottomMargin: CGFloat = {
var bottom: CGFloat = 0
if #available(iOS 11.0, *) {
bottom = UIApplication.shared.windows[0].safeAreaInsets.bottom
}
return bottom
}()
// 375
public let kSPMainW:((CGFloat)-> CGFloat) = { (size : CGFloat) -> CGFloat in
return kSPWidthScale * size
}
///
let kSPWidthScale = kSPScreenWidth / 375
//MARK:-------------- --------------
///
let kSP_osVersion: String = UIDevice.current.systemVersion
let kSPAPPBundleIdentifier: String = (Bundle.main.infoDictionary!["CFBundleIdentifier"] as? String) ?? "0"
///app
public let kSPAPPVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0"
public let kSPAPPBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0"
public let kSPAPPBundleName: String = (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? ""
//MARK: ------- ----------
public func spLog(message:Any? , file: String = #file, function: String = #function, line: Int = #line) {
#if DEBUG
print("\n\(Date(timeIntervalSinceNow: 8 * 60 * 60)) \(file.components(separatedBy: "/").last ?? "") \(function) \(line): \(message ?? "")")
#endif
}

View File

@ -0,0 +1,11 @@
//
// SPUserDefaultsKey.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
///
let kSPLoginTokenDefaultsKey = "kSPLoginTokenDefaultsKey"

View File

@ -0,0 +1,21 @@
//
// NSUserDefaults+JXAdd.h
// 链之家
//
// Created by 曾觉新 on 2017/11/16.
// Copyright © 2017年 NetLoanHome. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSUserDefaults (JXAdd)
///NSObject 需要实现NSSecureCoding协议
+ (void)jx_setObject:(nullable id)obj forKey:(NSString *)key;
+ (nullable id)jx_objectForKey:(NSString *)key class:(Class)cls;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,45 @@
//
// NSUserDefaults+JXAdd.m
//
//
// Created by on 2017/11/16.
// Copyright © 2017 NetLoanHome. All rights reserved.
//
#import "NSUserDefaults+JXAdd.h"
@implementation NSUserDefaults (JXAdd)
+ (void)jx_setObject:(nullable id)obj forKey:(NSString *)key {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (!obj) {
[defaults removeObjectForKey:key];
return;
}
if ([obj respondsToSelector:@selector(encodeWithCoder:)] == NO) {
NSLog(@"Error save object to NSUserDefaults. Object must respond to encodeWithCoder: message");
return;
}
NSError *error;
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:obj requiringSecureCoding:true error:&error];
// NSLog(@"%@", error);
[defaults setObject:encodedObject forKey:key];
[defaults synchronize];
}
+ (nullable id)jx_objectForKey:(NSString *)key class:(Class)cls {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedObject = [defaults objectForKey:key];
if (encodedObject && [encodedObject.class isSubclassOfClass:NSData.class]) {
NSError *error;
id obj = [NSKeyedUnarchiver unarchivedObjectOfClass:cls fromData:encodedObject error:&error];
// NSLog(@"%@", error);
return obj;
} else {
return nil;
}
}
@end

View File

@ -0,0 +1,28 @@
//
// String+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
import SmartCodable
extension String: SmartCodable {
func length() -> Int {
return self.ocString().length
}
func ocString() -> NSString {
return self as NSString
}
static func timeZone() -> String {
let timeZone = NSTimeZone.local as NSTimeZone
let timeZoneAbbreviation = timeZone.name.length() > 0 ? timeZone.name : "Unknown"
let timeZoneSecondsFromGMT = timeZone.secondsFromGMT / 3600
return String(format: "GMT+0%d:00", timeZoneSecondsFromGMT)
}
}

View File

@ -0,0 +1,47 @@
//
// UIColor+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
extension UIColor {
static func color(hex: UInt32, alpha: CGFloat = 1) -> UIColor {
return UIColor(rgb: hex, alpha: alpha)
}
// static func backgroundColor() -> UIColor {
// return colorF3F5F7()
// }
//
//
static func themeColor() -> UIColor {
return color000000()
}
//
// static func placeholderColor() -> UIColor {
// return color868C92()
// }
}
extension UIColor {
static func colorFFFFFF(alpha: CGFloat = 1) -> UIColor {
return color(hex: 0xFFFFFF, alpha: alpha)
}
static func color000000(alpha: CGFloat = 1) -> UIColor {
return color(hex: 0x000000, alpha: alpha)
}
static func colorFF0089(alpha: CGFloat = 1) -> UIColor {
return color(hex: 0xFF0089, alpha: alpha)
}
static func color7F7F80(alpha: CGFloat = 1) -> UIColor {
return color(hex: 0x7F7F80, alpha: alpha)
}
}

View File

@ -0,0 +1,73 @@
//
// UIDevice+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
extension UIDevice {
//http://theiphonewiki.com/wiki/Models
static func sp_machineModelName() -> String {
guard let machineModel = UIDevice.current.machineModel else { return "" }
let map = [
"iPhone1,1" : "iPhone",
"iPhone1,2" : "iPhone 3G",
"iPhone2,1" : "iPhone 3GS",
"iPhone3,1" : "iPhone 4",
"iPhone3,2" : "iPhone 4",
"iPhone3,3" : "iPhone 4",
"iPhone4,1" : "iPhone4,1",
"iPhone5,1" : "iPhone 5",
"iPhone5,2" : "iPhone 5",
"iPhone5,3" : "iPhone 5c",
"iPhone5,4" : "iPhone 5c",
"iPhone6,1" : "iPhone 5s",
"iPhone6,2" : "iPhone 5s",
"iPhone7,2" : "iPhone 6",
"iPhone7,1" : "iPhone 6 Plus",
"iPhone8,1" : "iPhone 6s",
"iPhone8,2" : "iPhone 6s Plus",
"iPhone8,4" : "iPhone SE (1st generation)",
"iPhone9,1" : "iPhone 7",
"iPhone9,3" : "iPhone 7",
"iPhone9,2" : "iPhone 7 Plus",
"iPhone9,4" : "iPhone 7 Plus",
"iPhone10,1" : "iPhone 8",
"iPhone10,4" : "iPhone 8",
"iPhone10,2" : "iPhone 8 Plus",
"iPhone10,5" : "iPhone 8 Plus",
"iPhone10,3" : "iPhone X",
"iPhone10,6" : "iPhone X",
"iPhone11,8" : "iPhone XR",
"iPhone11,2" : "iPhone11,2",
"iPhone11,6" : "iPhone XS Max",
"iPhone11,4" : "iPhone XS Max",
"iPhone12,1" : "iPhone 11",
"iPhone12,3" : "iPhone 11 Pro",
"iPhone12,5" : "iPhone 11 Pro Max",
"iPhone12,8" : "iPhone SE (2nd generation)",
"iPhone13,1" : "iPhone 12 mini",
"iPhone13,2" : "iPhone13,2",
"iPhone13,3" : "iPhone 12 Pro",
"iPhone13,4" : "iPhone 12 Pro Max",
"iPhone14,4" : "iPhone 13 mini",
"iPhone14,5" : "iPhone 13",
"iPhone14,2" : "iPhone 13 Pro",
"iPhone14,3" : "iPhone 13 Pro Max",
"iPhone14,6" : "iPhone SE (3rd generation)",
"iPhone14,7" : "iPhone 14",
"iPhone14,8" : "iPhone 14 Plus",
"iPhone15,2" : "iPhone 14 Pro",
"iPhone15,3" : "iPhone 14 Pro Max",
]
if let name = map[machineModel] {
return name
}
return machineModel
}
}

View File

@ -0,0 +1,23 @@
//
// UIFont+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
extension UIFont {
static func fontRegular(ofSize: CGFloat) -> UIFont {
return .systemFont(ofSize: ofSize, weight: .regular)
}
static func fontMedium(ofSize: CGFloat) -> UIFont {
return .systemFont(ofSize: ofSize, weight: .medium)
}
static func fontBold(ofSize: CGFloat) -> UIFont {
return .systemFont(ofSize: ofSize, weight: .bold)
}
}

View File

@ -0,0 +1,24 @@
//
// UIImageView+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
import Kingfisher
extension UIImageView {
func sp_setImage(url: String?, placeholder: UIImage? = nil, completer: ((_ image: UIImage?, _ url: URL?) -> Void)? = nil) {
self.kf.setImage(with: URL(string: url ?? ""), placeholder: placeholder, options: nil) { result in
switch result {
case .success(let value):
completer?(value.image, value.source.url)
default :
completer?(nil, nil)
break
}
}
}
}

View File

@ -0,0 +1,92 @@
//
// UINavigationBar+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
extension UINavigationBar {
static let sp_normalTitleFont = UIFont.fontBold(ofSize: 15)
static var sp_normalTitleColor: UIColor {
get {
return .colorFFFFFF()
}
}
/**
*/
static var sp_normalTitleTextAttributes: [NSAttributedString.Key : Any] {
get {
return [
NSAttributedString.Key.font : sp_normalTitleFont,
NSAttributedString.Key.foregroundColor : sp_normalTitleColor
]
}
}
/**
*/
static var sp_normalBackgroundColor: UIColor {
get {
return .themeColor()
}
}
@available(iOS 13.0, *)
static let navBarAppearance: UINavigationBarAppearance = {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithOpaqueBackground()
//
navBarAppearance.backgroundColor = sp_normalBackgroundColor
//
navBarAppearance.backgroundEffect = nil
// clear线
navBarAppearance.shadowColor = UIColor.clear
//
navBarAppearance.titleTextAttributes = sp_normalTitleTextAttributes
return navBarAppearance
}()
func sp_setTranslucent(isTranslucent: Bool) {
self.isTranslucent = isTranslucent
}
func sp_setBackgroundColor(backgroundColor: UIColor?) {
if #available(iOS 15.0, *) {
UINavigationBar.navBarAppearance.backgroundColor = backgroundColor
self.standardAppearance = UINavigationBar.navBarAppearance
self.scrollEdgeAppearance = UINavigationBar.navBarAppearance
}
if let backgroundColor = backgroundColor {
self.setBackgroundImage(UIImage(color: backgroundColor), for: .default)
// self.barTintColor = backgroundColor
} else {
self.setBackgroundImage(UIImage(), for: .default)
// self.barTintColor = nil
}
}
func sp_setTitleTextAttributes(titleTextAttributes: [NSAttributedString.Key : Any]?) {
if #available(iOS 15.0, *) {
if let titleTextAttributes = titleTextAttributes {
UINavigationBar.navBarAppearance.titleTextAttributes = titleTextAttributes
}
self.scrollEdgeAppearance = UINavigationBar.navBarAppearance
self.standardAppearance = UINavigationBar.navBarAppearance
} else {
self.titleTextAttributes = titleTextAttributes
}
}
}

View File

@ -0,0 +1,30 @@
//
// UINavigationController+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
extension UINavigationController {
/**
*/
func pushViewControllerAndDismissLastViewController(viewController: UIViewController, animated: Bool) {
var viewControllers = self.viewControllers
viewControllers.removeLast()
viewControllers.append(viewController)
self.setViewControllers(viewControllers, animated: animated)
}
//MARK:-------------- --------------
open override var childForStatusBarStyle: UIViewController? {
return self.topViewController
}
open override var childForStatusBarHidden: UIViewController? {
return self.topViewController
}
}

View File

@ -0,0 +1,15 @@
//
// UIView+SPAdd.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
import SnapKit
extension UIView {
}

View File

@ -0,0 +1,22 @@
//
// SPListModel.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
import SmartCodable
class SPListModel<T: SmartCodable>: SPModel, SmartCodable {
var list: [T]?
var pagination: SPListPaginationModel?
}
class SPListPaginationModel: SPModel, SmartCodable {
var current_page: Int?
var page_size: Int?
var page_total: Int?
var total_size: Int?
}

View File

@ -0,0 +1,15 @@
//
// SPModel.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
import SmartCodable
class SPModel: NSObject {
required override init() {
}
}

View File

@ -0,0 +1,27 @@
//
// SPHomeAPI.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPHomeAPI: NSObject {
///
static func requestRecommandsTV(page: Int, completer: ((_ listModel: SPListModel<SPShortModel>?) -> Void)?) {
var param = SPNetworkParameters(path: "/getRecommands")
param.method = .get
param.parameters = [
"page_size" : 20,
"current_page" : page
]
SPNetwork.request(parameters: param) { (response: SPNetworkResponse<SPListModel<SPShortModel>>) in
completer?(response.data)
}
}
}

View File

@ -0,0 +1,117 @@
//
// SPApi.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
import Moya
import SmartCodable
struct SPNetworkData<T: SmartCodable> {
var parameters: SPNetworkParameters?
var completion: ((_ response: SPNetworkResponse<T>) -> Void)?
}
struct SPNetworkParameters {
var baseURL: URL?
var parameters: [String : Any]?
var method: Moya.Method = .post
var path: String
var isLoding: Bool = false
var isToast: Bool = true
}
struct SPNetworkResponse<T: SmartCodable>: SmartCodable {
var code: Int?
var data: T?
var msg: String?
///
@IgnoredKey
var rawData: Any?
}
enum SPApi {
case request(parameters: SPNetworkParameters)
case uploadFile(parameters: SPNetworkParameters)
case downloadFile(parameters: SPNetworkParameters)
}
extension SPApi: TargetType {
var baseURL: URL {
return URL(string: SPBaseURL)!
}
var path: String {
switch self {
case .request(let parameters):
return parameters.path
default:
return ""
}
}
var method: Moya.Method {
switch self {
case .request(let parameters):
return parameters.method
default:
return .post
}
}
var task: Moya.Task {
switch self {
case .request(let parameters):
let p = parameters.parameters ?? [:]
return .requestParameters(parameters: p, encoding: getEncoding())
default:
return .requestParameters(parameters: [:], encoding: getEncoding())
}
}
var headers: [String : String]? {
let userToken = SPLoginManager.manager.token?.token ?? ""
var dic: [String : String] = [
"system-version" : kSP_osVersion,
"lang-key" : SPLocalizedManager.shared.currentLocalizedKey,//
"time-zone" : String.timeZone(), //
"app-version" : kSPAPPVersion,
"device-id" : JXUUID.systemUUID(), //id
"brand" : "apple", //
"app-name" : "",
"system-type" : "ios",
"idfa" : JXUUID.idfa(),
"model" : UIDevice.sp_machineModelName(),
]
//
dic["authorization"] = userToken
return dic
}
}
extension TargetType {
var sampleData: Data { return "".data(using: String.Encoding.utf8)! }
func getEncoding() -> ParameterEncoding {
switch self.method {
case .get, .delete:
return URLEncoding.default
default:
return JSONEncoding.default
}
}
}

View File

@ -0,0 +1,179 @@
//
// SPNetwork.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
import Moya
import SmartCodable
///
let SPNetworkCodeSucceed = 200
class SPNetwork: NSObject {
///
private static var awaitDataArr: [Any] = []
private static var group = DispatchGroup()
private static var queue: DispatchQueue = DispatchQueue(label: "bcRequestQueue", attributes: .concurrent)
static let semaphore = DispatchSemaphore(value: 1) // 1
static let provider = MoyaProvider<SPApi>(requestClosure: CustomApiTimeoutClosure)
static func request<T>(parameters: SPNetworkParameters, completion: ((_ response: SPNetworkResponse<T>) -> Void)?) {
if SPLoginManager.manager.token == nil {
SPLoginManager.manager.requestVisitorLogin(completer: nil)
}
if SPLoginManager.manager.isRefreshingToken && parameters.path != "/customer/register" {
var loding = true
while loding {
if !SPLoginManager.manager.isRefreshingToken {
loding = false
}
RunLoop.current.run(mode: .default, before: Date.distantFuture)
}
}
_request(parameters: parameters, completion: completion)
}
@discardableResult
static func _request<T>(parameters: SPNetworkParameters, completion: ((_ response: SPNetworkResponse<T>) -> Void)?) -> Cancellable {
if parameters.isLoding {
// ETHUD.show()
}
return provider.requestCustomJson(.request(parameters: parameters)) { (result) in
if parameters.isLoding {
// ETHUD.dismiss()
}
guard let completion = completion else {return}
_resultDispose(parameters: parameters, result: result, completion: completion)
}
}
private static func _resultDispose<T>(parameters: SPNetworkParameters, result: Result<Moya.Response, MoyaError>, completion: ((_ response: SPNetworkResponse<T>) -> Void)?) {
switch result {
case .success(let response):
let code = response.statusCode
if code == 401 || code == 402 || code == 403 {
// var data = SPNetworkData<T>()
// data.parameters = parameters
// data.completion = completion
//
// awaitDataArr.append(data)
// awaitDataArr.first as? SPNetworkData<T>
if !SPLoginManager.manager.isRefreshingToken {
SPLoginManager.manager.requestVisitorLogin {
if let _ = SPLoginManager.manager.token {
self.request(parameters: parameters, completion: completion)
}
}
} else {
if parameters.path != "/customer/register" {
// while SPLoginManager.manager.isRefreshingToken {
// RunLoop.current.run(mode: .default, before: Date.distantFuture)
// }
// if let _ = SPLoginManager.manager.token {
// self.request(parameters: parameters, completion: completion)
// }
}
}
return
}
do {
let tempData: [String : Any] = try response.mapJSON() as! [String : Any]
spLog(message: parameters.parameters)
spLog(message: parameters.path)
spLog(message: tempData as NSDictionary)
var response = SPNetworkResponse<T>.deserialize(from: tempData)
if response != nil {
if response?.code == SPNetworkCodeSucceed {
} else {
if parameters.isToast {
SPToast.show(text: response?.msg)
}
}
response?.rawData = tempData
completion?(response!)
} else {
response = SPNetworkResponse<T>()
response?.code = -1
if parameters.isToast {
SPToast.show(text: "Error".localized)
// ETHUD.showToast(text: "".localized)
}
completion?(response!)
}
} catch {
var response = SPNetworkResponse<T>()
response.code = -1
if parameters.isToast {
SPToast.show(text: "Error".localized)
// ETHUD.showToast(text: "".localized)
}
completion?(response)
}
case .failure(let error):
spLog(message: error)
var response = SPNetworkResponse<T>()
response.code = -1
if parameters.isToast {
SPToast.show(text: "Error".localized)
// ETHUD.showToast(text: "".localized)
}
completion?(response)
break
}
}
}
extension MoyaProvider {
@discardableResult
func requestCustomJson(_ target: Target, callbackQueue: DispatchQueue? = nil, completion: Completion?) -> Cancellable {
return request(target, callbackQueue: callbackQueue) { (result) in
guard let completion = completion else {return}
completion(result)
}
}
}
let CustomApiTimeoutClosure = {(endpoint: Endpoint, closure: MoyaProvider<SPApi>.RequestResultClosure) -> Void in
if var urlRequest = try? endpoint.urlRequest() {
///
urlRequest.cachePolicy = .reloadIgnoringCacheData
urlRequest.timeoutInterval = 30
closure(.success(urlRequest))
} else {
closure(.failure(MoyaError.requestMapping(endpoint.url)))
}
#if DEBUG ///
//print(try? endpoint.urlRequest() )
#endif
}

View File

@ -0,0 +1,18 @@
//
// SPURLPath.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
#if DEBUG
let SPBaseURL = "https://test1-api.guyantv.com"
let SPWebBaseURL = "https://test1-api.guyantv.com"
#else
let SPBaseURL = "https://test1-api.guyantv.com"
let SPWebBaseURL = "https://test1-api.guyantv.com"
#endif

View File

@ -0,0 +1,22 @@
//
// SPCollectionView.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPCollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.backgroundColor = .clear
self.contentInsetAdjustmentBehavior = .never
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,42 @@
//
// SPCollectionViewCell.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.rasterizationScale = UIScreen.main.scale
self.layer.shouldRasterize = true
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
extension UICollectionViewCell {
// MARK: -
public static func registerCell(collectionView: UICollectionView, _ reuseIdentifier: String = "reuseIdentifier") {
let reuseIdentifier = reuseIdentifier == "reuseIdentifier" ? NSStringFromClass(self) : reuseIdentifier
collectionView.register(self, forCellWithReuseIdentifier: reuseIdentifier)
}
public static func registerNibCell(collectionView: UICollectionView, _ reuseIdentifier: String = "reuseIdentifier") {
let reuseIdentifier = reuseIdentifier == "reuseIdentifier" ? NSStringFromClass(self) : reuseIdentifier
collectionView.register(UINib(nibName: "\(self)", bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
}
// MARK: -
public static func dequeueReusableCell(collectionView: UICollectionView, indexPath: IndexPath , _ reuseIdentifier: String = "reuseIdentifier") -> Self{
let reuseIdentifier = reuseIdentifier == "reuseIdentifier" ? NSStringFromClass(self) : reuseIdentifier
return collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! Self
}
}

View File

@ -0,0 +1,54 @@
//
// SPImageView.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPImageView: UIImageView {
var placeholderColor = UIColor.gray
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
override init(image: UIImage?) {
super.init(image: image)
_init()
}
override init(image: UIImage?, highlightedImage: UIImage?) {
super.init(image: image, highlightedImage: highlightedImage)
_init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func _init() {
self.contentMode = .scaleAspectFill
self.layer.masksToBounds = true
if image == nil {
self.backgroundColor = self.placeholderColor
}
}
override var image: UIImage? {
didSet {
if self.backgroundColor == nil && image == nil {
self.backgroundColor = self.placeholderColor
} else if image != nil {
if self.backgroundColor == self.placeholderColor {
self.backgroundColor = nil
}
}
}
}
}

View File

@ -0,0 +1,46 @@
//
// SPForYouViewController.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPForYouViewController: SPPlayerListViewController {
override func viewDidLoad() {
super.viewDidLoad()
requestDataArr(page: 1)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
extension SPForYouViewController {
private func requestDataArr(page: Int) {
SPHomeAPI.requestRecommandsTV(page: page) {[weak self] listModel in
guard let self = self else { return }
if let listModel = listModel, let list = listModel.list {
if page == 1 {
self.setDataArr(dataArr: list)
self.play()
} else {
}
self.pagination = listModel.pagination
}
}
}
}

View File

@ -0,0 +1,58 @@
//
// SPHomePageController.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPHomePageController: SPViewController {
private lazy var pageView: JYPageController = {
let pageView = JYPageController()
pageView.delegate = self
pageView.dataSource = self
return pageView
}()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
}
//MARK: -------------- JYPageControllerDelegate & JYPageControllerDataSource --------------
extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource {
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {
return .init(x: 0, y: kSPStatusbarHeight + 10, width: kSPScreenWidth, height: 40)
}
func pageController(_ pageController: JYPageController, frameForContainerView container: UIScrollView) -> CGRect {
return .init(x: 0, y: 0, width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight)
}
func pageController(_ pageController: JYPageController, titleAt index: Int) -> String {
return "123"
}
func childController(atIndex index: Int) -> any JYPageChildContollerProtocol {
return SPViewController()
}
func numberOfChildControllers() -> Int {
return 0
}
}

View File

@ -0,0 +1,21 @@
//
// SPHomeViewController.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPHomeViewController: SPViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}

View File

@ -0,0 +1,16 @@
//
// SPHomeCategoryModel.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPHomeCategoryModel: SPModel {
var category_name: String?
var category_id: String?
}

View File

@ -0,0 +1,177 @@
//
// SPPlayerListViewController.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPPlayerListViewController: SPViewController {
var contentSize: CGSize {
return CGSize(width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight)
}
private var dataArr: [Any] = []
var pagination: SPListPaginationModel?
private var viewModel = SPPlayerListViewModel()
private(set) var currentIndexPath = IndexPath(row: 0, section: 0)
private lazy var collectionViewLayout: UICollectionViewLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = contentSize
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
return layout
}()
private lazy var collectionView: SPCollectionView = {
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isPagingEnabled = true
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.bounces = false
collectionView.scrollsToTop = false
SPPlayerListCell.registerCell(collectionView: collectionView)
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
//
do {
try? KTVHTTPCache.proxyStart()
}
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
sp_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.dataArr.isEmpty && self.viewModel.isPlaying {
self.viewModel.currentPlayer?.start()
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.viewModel.currentPlayer?.pause()
}
func setDataArr(dataArr: [Any]) {
self.dataArr = dataArr
CATransaction.begin()
self.collectionView.reloadData()
CATransaction.commit()
}
func clearDataArr() {
self.dataArr.removeAll()
self.viewModel.currentPlayer = nil
self.collectionView.reloadData()
}
func play() {
if self.isDidAppear {
self.viewModel.currentPlayer?.start()
}
self.viewModel.isPlaying = true
if self.dataArr.count - currentIndexPath.row <= 2 {
// self.loadMoreData()
}
if currentIndexPath.row <= 2 {
// self.loadUpMoreData()
}
}
}
extension SPPlayerListViewController {
private func sp_setupUI() {
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = SPPlayerListCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
cell.model = dataArr[indexPath.row]
if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath {
self.currentIndexPath = indexPath
self.viewModel.currentPlayer = cell
}
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
//
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let indexPaths = self.collectionView.indexPathsForVisibleItems
for indexPath in indexPaths {
guard let cell = self.collectionView.cellForItem(at: indexPath) else { continue }
if offsetY == cell.frame.origin.y {
if self.currentIndexPath != indexPath {
self.skip(indexPath: indexPath)
}
}
}
}
private func skip(indexPath: IndexPath) {
currentIndexPath = indexPath
guard let currentPlayer = self.collectionView.cellForItem(at: indexPath) as? SPPlayerProtocol else { return }
self.viewModel.currentPlayer = currentPlayer
// currentCell = self.collectionView.cellForItem(at: indexPath) as? BCListPlayerCell
self.play()
}
}
//MARK: -------------- APP --------------
extension SPPlayerListViewController {
@objc func didBecomeActiveNotification() {
if !self.dataArr.isEmpty && self.viewModel.isPlaying && isDidAppear {
self.viewModel.currentPlayer?.start()
}
}
@objc func willResignActiveNotification() {
self.viewModel.currentPlayer?.pause()
}
}

View File

@ -0,0 +1,28 @@
//
// SPPlayerProtocol.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
protocol SPPlayerProtocol: NSObjectProtocol {
///
var playerFinishHadle: (() -> Void)? { get set }
var model: Any? { get set }
var isCurrent: Bool { get set }
///
func prepare()
///
func start()
///
func pause()
}

View File

@ -0,0 +1,37 @@
//
// SPShortModel.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
import SmartCodable
class SPShortModel: SPModel, SmartCodable {
var all_coins: String?
var buy_type: String?
var collect_total: String?
var sp_description: String?
var episode_total: Int?
var horizontally_img: String?
var id: String?
var image_url: String?
var is_collect: Bool?
var name: String?
var process: String?
var search_click_total: String?
var short_id: String?
var short_play_id: String?
var tag_type: String?
var video_info: SPVideoInfoModel?
var watch_total: Int?
static func mappingForKey() -> [SmartKeyTransformer]? {
return [
CodingKeys.sp_description <--- ["description"]
]
}
}

View File

@ -0,0 +1,26 @@
//
// SPVideoInfoModel.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
import SmartCodable
class SPVideoInfoModel: SPModel, SmartCodable {
var coins: Int?
var vip_coins: Int?
var episode: Int?
var id: String?
var image_url: String?
var is_vip: Int?
var promise_view_ad: Int?
// var revolution: []
var short_id: String?
var short_play_id: String?
var short_play_video_id: String?
var video_url: String?
}

View File

@ -0,0 +1,97 @@
//
// SPPlayerControlView.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPPlayerControlView: UIView {
///
var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)?
///0-1
var progress: CGFloat = 0 {
didSet {
progressView.progress = progress
}
}
private(set) lazy var progressView: SPPlayerProgressView = {
let view = SPPlayerProgressView()
view.panStart = { [weak self] in
guard let self = self else { return }
self.panProgressStart()
}
view.panChange = { [weak self] progress in
guard let self = self else { return }
self.panProgressChange(progress: progress)
}
view.panFinish = { [weak self] progress in
guard let self = self else { return }
self.panProgressFinish(progress: progress)
}
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
sp_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SPPlayerControlView {
private func sp_setupUI() {
addSubview(progressView)
progressView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-20)
make.height.equalTo(30)
}
}
}
extension SPPlayerControlView {
///
private func panProgressStart() {
// self.isHidden = true
// screenProgressView.duration = self.player?.duration ?? 0
// screenProgressView.frame = self.window?.bounds ?? .zero
//
// self.window?.addSubview(screenProgressView)
//
// var point = self.progressView.convert(CGPoint(x: 0, y: 0), to: self.window)
// point.y = point.y + self.progressView.size.height - self.progressView.lineWidth / 2
//
// screenProgressView.point = point
}
///
private func panProgressChange(progress: CGFloat) {
// screenProgressView.progress = progress
}
///
private func panProgressFinish(progress: CGFloat) {
// self.isHidden = false
// screenProgressView.removeFromSuperview()
self.panProgressFinishBlock?(progress)
}
}

View File

@ -0,0 +1,121 @@
//
// SPPlayerListCell.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
private lazy var player: SPPlayer = {
let player = SPPlayer()
player.playerView = playerView
player.delegate = self
return player
}()
private lazy var playerView: UIView = {
let view = UIView()
return view
}()
private lazy var coverImageView: SPImageView = {
let imageView = SPImageView()
return imageView
}()
private lazy var controlView: SPPlayerControlView = {
let view = SPPlayerControlView()
view.panProgressFinishBlock = { [weak self] progress in
guard let self = self else { return }
let duration = CGFloat(self.player.duration)
let toTime = progress * duration
self.player.seekToTime(toTime: Int(toTime))
}
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
sp_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: SPPlayerProtocol
var model: Any? {
didSet {
guard let model = model as? SPShortModel else { return }
player.setPlayUrl(url: model.video_info?.video_url ?? "")
coverImageView.sp_setImage(url: model.image_url)
}
}
var isCurrent: Bool = false
///
var playerFinishHadle: (() -> Void)?
func prepare() {
// player.prepare()
}
func start() {
player.start()
}
func pause() {
player.pause()
}
}
extension SPPlayerListCell {
private func sp_setupUI() {
contentView.addSubview(coverImageView)
contentView.addSubview(playerView)
contentView.addSubview(controlView)
coverImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
playerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
controlView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
//MARK: -------------- SPPlayerDelegate --------------
extension SPPlayerListCell: SPPlayerDelegate {
func sp_playCompletion(_ player: SPPlayer) {
}
func sp_playLoadingEnd(_ player: SPPlayer) {
}
func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int) {
controlView.progress = CGFloat(currentTime) / CGFloat(duration)
}
func sp_firstRenderedStart(_ player: SPPlayer) {
}
}

View File

@ -0,0 +1,125 @@
//
// SPPlayerProgressView.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPPlayerProgressView: UIView {
///
var panStart: (() -> Void)?
///
var panChange: ((_ progress: CGFloat) -> Void)?
///
var panFinish: ((_ progress: CGFloat) -> Void)?
var progress: CGFloat = 0 {
didSet {
if !isPaning {
setNeedsDisplay()
}
}
}
///
private var tempProgress: CGFloat = 0
///
private var panProgress: CGFloat = 0
var progressColor: UIColor = .red
var currentProgress: UIColor = .white
var lineWidth: CGFloat = 2
///
private var isPaning: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
self.addGestureRecognizer(pan)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:)))
self.addGestureRecognizer(tap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
let width = rect.width
let height = rect.height
var progress = self.progress
if self.isPaning {
progress = self.panProgress
}
///
let progressPath = UIBezierPath(roundedRect: CGRect(x: 0, y: height - lineWidth, width: width, height: lineWidth), cornerRadius: lineWidth / 2)
context.addPath(progressPath.cgPath)
context.setFillColor(progressColor.cgColor)
context.fillPath()
///
let currentPath = UIBezierPath(roundedRect: CGRect(x: 0, y: height - lineWidth, width: width * progress, height: lineWidth), cornerRadius: lineWidth / 2)
context.addPath(currentPath.cgPath)
context.setFillColor(currentProgress.cgColor)
context.fillPath()
}
}
extension SPPlayerProgressView {
@objc func handlePanGesture(sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
self.isPaning = true
self.tempProgress = self.progress
sender.setTranslation(CGPoint(x: 0, y: 0), in: self)
self.panStart?()
case .changed:
let point = sender.translation(in: self)
let offsetX = point.x / self.width
self.panProgress = self.tempProgress + offsetX
if self.panProgress < 0 {
self.panProgress = 0
}
self.panChange?(self.panProgress)
setNeedsDisplay()
default:
self.isPaning = false
self.panFinish?(self.panProgress)
self.panProgress = 0
}
}
@objc func handleTapGesture(sender: UITapGestureRecognizer) {
let point = sender.location(in: self)
let offsetX = point.x / self.width
self.panFinish?(offsetX)
}
}

View File

@ -0,0 +1,29 @@
//
// SPPlayerListViewModel.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPPlayerListViewModel: NSObject {
var isPlaying = false
private var _currentPlayer: SPPlayerProtocol?
var currentPlayer: SPPlayerProtocol? {
set {
_currentPlayer?.isCurrent = false
_currentPlayer?.pause()
_currentPlayer = newValue
_currentPlayer?.isCurrent = true
}
get {
return _currentPlayer
}
}
}

View File

@ -0,0 +1,93 @@
//
// SPAPPTool.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPAPPTool: NSObject {
static func getAppDelegate() -> AppDelegate? {
return UIApplication.shared.delegate as? AppDelegate
}
///
static func getLanuchViewController() -> UIViewController {
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "LaunchScreen")
return vc
}
///
static func rootViewController() -> UIViewController? {
return getKeyWindow()?.rootViewController
}
///
static func topViewController() -> UIViewController? {
var resultVC: UIViewController? = self.rootViewController()
if let rootNav = resultVC as? UINavigationController {
resultVC = rootNav.topViewController
}
resultVC = self._topViewController(resultVC)
while resultVC?.presentedViewController != nil {
resultVC = self._topViewController(resultVC?.presentedViewController)
}
return resultVC
}
private static func _topViewController(_ vc: UIViewController?) -> UIViewController? {
if vc is UINavigationController {
return _topViewController((vc as? UINavigationController)?.topViewController)
} else if vc is UITabBarController {
return _topViewController((vc as? UITabBarController)?.selectedViewController)
} else {
return vc
}
}
static func getKeyWindow() -> UIWindow? {
var window: UIWindow?
if #available(iOS 13.0, *) {
window = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.map({$0 as? UIWindowScene})
.compactMap({$0})
.first?.windows
.filter({$0.isKeyWindow}).first
}
if window == nil {
window = UIApplication.shared.windows.first { $0.isKeyWindow }
}
if window == nil {
window = UIApplication.shared.keyWindow
}
return window
}
}
extension SPAPPTool {
///
static func copy(text: String?) {
if let text = text {
let copy = UIPasteboard.general
copy.string = text
}
}
/**
*/
static func impactFeedbackOccurred() {
if #available(iOS 13.0, *) {
UIImpactFeedbackGenerator(style: .heavy).impactOccurred(intensity: 1)
} else {
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
}
}
}

View File

@ -0,0 +1,17 @@
//
// SPToast.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
class SPToast: NSObject {
static func show(text: String?) {
guard let text = text else { return }
SPAPPTool.getKeyWindow()?.makeToast(text, duration: 2, position: nil)
}
}

View File

@ -0,0 +1,57 @@
//
// SPLoginManager.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPLoginManager: NSObject {
static let manager = SPLoginManager()
private(set) var token: SPTokenModel?
///token
private(set) var isRefreshingToken = false
override init() {
super.init()
token = UserDefaults.jx_object(forKey: kSPLoginTokenDefaultsKey, class: SPTokenModel.self) as? SPTokenModel
}
func setLoginToken(token: SPTokenModel?) {
self.token = token
UserDefaults.jx_setObject(token, forKey: kSPLoginTokenDefaultsKey)
}
}
extension SPLoginManager {
///
func requestVisitorLogin(completer: (() -> Void)?) {
if isRefreshingToken {
return
}
isRefreshingToken = true
// var loding = true
let param = SPNetworkParameters(path: "/customer/register")
SPNetwork.request(parameters: param) { [weak self] (response: SPNetworkResponse<SPTokenModel>) in
guard let self = self else { return }
if let token = response.data {
self.setLoginToken(token: token)
}
self.isRefreshingToken = false
// loding = false
completer?()
}
// while loding {
// RunLoop.current.run(mode: .default, before: Date.distantFuture)
// }
}
}

View File

@ -0,0 +1,39 @@
//
// SPTokenModel.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
import SmartCodable
class SPTokenModel: SPModel, SmartCodable, NSSecureCoding {
var token: String?
var customer_id: String?
var auto_login: Int?
required init() { }
static var supportsSecureCoding: Bool {
get {
return true
}
}
func encode(with coder: NSCoder) {
coder.encode(token, forKey: "token")
coder.encode(customer_id, forKey: "customer_id")
coder.encode(auto_login, forKey: "auto_login")
}
required init?(coder: NSCoder) {
super.init()
token = coder.decodeObject(of: NSString.self, forKey: "token") as? String
customer_id = coder.decodeObject(of: NSString.self, forKey: "customer_id") as? String
auto_login = coder.decodeObject(of: NSNumber.self, forKey: "auto_login")?.intValue
}
}

View File

@ -0,0 +1,147 @@
//
// SPPlayer.swift
// ShortPlay
//
// Created by on 2025/4/9.
//
import UIKit
import ZFPlayer
@objc protocol SPPlayerDelegate {
///
// @objc optional func sp_onDurationUpdate(_ player: SPPlayer, duration: Int)
//
// ///
// @objc optional func sp_onCurrentPositionUpdate(_ player: SPPlayer, position: Int)
///
@objc optional func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int)
///
@objc optional func sp_firstRenderedStart(_ player: SPPlayer)
///
@objc optional func sp_playCompletion(_ player: SPPlayer)
///
@objc optional func sp_playLoadingEnd(_ player: SPPlayer)
}
class SPPlayer: NSObject {
weak var delegate: SPPlayerDelegate?
private(set) lazy var isPlaying = false
///
var duration: Int {
return Int(self.player.totalTime)
}
///
var currentPosition: Int {
return Int(self.player.currentTime)
}
var playerView: UIView? {
didSet {
playerView?.addSubview(player.view)
player.view.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
private lazy var player: ZFAVPlayerManager = {
let player = ZFAVPlayerManager()
return player
}()
var isLoop = true
override init() {
super.init()
player.scalingMode = .aspectFill
sp_addAction()
}
func setPlayUrl(url: String) {
let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url))
self.player.assetURL = proxyURL
// self.player.assetURL = URL(string: url)
self.prepare()
}
///
func prepare() {
self.player.prepareToPlay()
}
func stop() {
self.isPlaying = false
player.stop()
}
func start() {
self.isPlaying = true
player.play()
}
///
func pause() {
self.isPlaying = false
player.pause()
}
func seekToTime(toTime: Int) {
// self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE)
self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil)
}
}
extension SPPlayer {
private func sp_addAction() {
//
player.playerPlayTimeChanged = { [weak self] (asset, currentTime, duration) in
guard let self = self else { return }
self.delegate?.sp_playTimeChanged?(self, currentTime: Int(currentTime), duration: Int(duration))
}
//
player.playerPlayStateChanged = { [weak self] (asset, playState) in
guard let self = self else { return }
if playState == .playStatePlaying, !isPlaying {
self.pause()
}
spLog(message: "播放状态====\(playState)")
}
//
player.playerLoadStateChanged = { [weak self] (asset, loadState) in
guard let self = self else { return }
if loadState == .playable, !isPlaying {
self.pause()
}
spLog(message: "加载状态====\(loadState)")
}
//
player.playerPlayFailed = { [weak self] (asset, error) in
spLog(message: "错误信息====\(error)")
}
//
player.playerDidToEnd = { [weak self] (asset) in
guard let self = self else { return }
if isLoop {
self.player.replay()
} else {
self.delegate?.sp_playCompletion?(self)
}
}
}
}

View File

@ -0,0 +1,61 @@
//
// SPLocalizedManager.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
class SPLocalizedManager: NSObject {
static let shared = SPLocalizedManager()
private let userDefaultsKey = "AppLocalized"
//
var currentLocalizedKey: String {
get {
// return UserDefaults.standard.string(forKey: userDefaultsKey) ?? Locale.preferredLanguages.first ?? "en"
return "en"
}
set {
UserDefaults.standard.set(newValue, forKey: userDefaultsKey)
UserDefaults.standard.synchronize()
NotificationCenter.default.post(name: SPLocalizedManager.localizedDidChange, object: nil)
}
}
//
var isFollowingSystem: Bool {
return UserDefaults.standard.string(forKey: userDefaultsKey) == nil
}
//
func resetToSystemLanguage() {
UserDefaults.standard.removeObject(forKey: userDefaultsKey)
UserDefaults.standard.synchronize()
}
//
func localizedString(forKey key: String, tableName: String? = nil) -> String {
if let selectedLanguage = UserDefaults.standard.string(forKey: userDefaultsKey),
let bundlePath = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj"),
let bundle = Bundle(path: bundlePath) {
return bundle.localizedString(forKey: key, value: nil, table: tableName)
} else {
return NSLocalizedString(key, tableName: tableName, bundle: .main, value: "", comment: "")
}
}
}
extension SPLocalizedManager {
static let localizedDidChange = Notification.Name(rawValue: "SPLocalizedManager.localizedDidChange")
}
extension String {
var localized: String {
return SPLocalizedManager.shared.localizedString(forKey: self)
}
}

View File

@ -0,0 +1,44 @@
//
// SPUserInfo.swift
// ShortPlay
//
// Created by on 2025/4/8.
//
import UIKit
import SmartCodable
class SPUserInfo: SPModel, SmartCodable, NSSecureCoding {
required init() { }
static var supportsSecureCoding: Bool {
get {
return true
}
}
func encode(with coder: NSCoder) {
// coder.encode(id, forKey: "id")
// coder.encode(phone, forKey: "phone")
// coder.encode(userToken, forKey: "userToken")
// coder.encode(ipAddress, forKey: "ipAddress")
// coder.encode(audioNum, forKey: "audioNum")
// coder.encode(audioSeconds, forKey: "audioSeconds")
}
required init?(coder: NSCoder) {
super.init()
// id = coder.decodeObject(of: NSString.self, forKey: "id") as? String
// phone = coder.decodeObject(of: NSString.self, forKey: "phone") as? String
// userToken = coder.decodeObject(of: NSString.self, forKey: "userToken") as? String
// ipAddress = coder.decodeObject(of: NSString.self, forKey: "ipAddress") as? String
// audioNum = coder.decodeObject(of: NSNumber.self, forKey: "audioNum")?.intValue
// audioSeconds = coder.decodeObject(of: NSNumber.self, forKey: "audioSeconds")?.intValue
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Frame@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Frame@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,25 @@
<?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>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
//
// ShortPlay-Bridging-Header.h
// ShortPlay
//
// Created by 曾觉新 on 2025/4/8.
//
#import "JXUUID.h"
#import <YYKit/YYKit.h>
#import <ZFPlayer.h>
#import <Toast.h>
#import "NSUserDefaults+JXAdd.h"
#import <KTVHTTPCache.h>

View File

@ -0,0 +1,11 @@
/*
Localizable.strings
ShortPlay
Created by 曾觉新 on 2025/4/8.
英语
*/
"Home" = "Home";
"For You" = "For You";
"Error" = "Error";

View File

@ -0,0 +1,86 @@
//
// JXBaseAnimatedTransition.swift
// Test
//
// Created by on 2022/10/10.
//
import UIKit
class JXBaseAnimatedTransition: NSObject {
open var isHideTabBar = false
open var transitionContext: UIViewControllerContextTransitioning?
open weak var containerView: UIView?
open weak var fromViewController: UIViewController?
open weak var toViewController: UIViewController?
open var contentView: UIView?
// open var isScale = false
// open var shadowView: UIView!
// open var isHideTabBar = false
// open var contentView: UIView?
open func getCapture(with view: UIView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
extension JXBaseAnimatedTransition: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(UINavigationController.hideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromVC = transitionContext.viewController(forKey: .from)
let toVC = transitionContext.viewController(forKey: .to)
self.containerView = containerView
self.fromViewController = fromVC
self.toViewController = toVC
self.transitionContext = transitionContext
animateTransition()
}
@objc open func animateTransition() {
// SubClass Implementation
}
public func completeTransition() {
guard let transitionContext = self.transitionContext else { return }
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
public func animationDuration() -> TimeInterval {
return self.transitionDuration(using: self.transitionContext)
}
}
extension UIViewController {
fileprivate struct AssociatedKeys {
static var defCaptureImage: Int?
}
public var jx_captureImage: UIImage? {
get {
guard let obj = objc_getAssociatedObject(self, &AssociatedKeys.defCaptureImage) else { return nil }
return obj as? UIImage
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.defCaptureImage, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

View File

@ -0,0 +1,202 @@
//
// JXNavigationInteractiveTransition.swift
// Test
//
// Created by on 2022/10/10.
//
import UIKit
class JXNavigationInteractiveTransition: NSObject {
var interactionController: UIPercentDrivenInteractiveTransition?
var isGesturePush = false
weak var navigationController: UINavigationController!
weak var visibleVC: UIViewController?
}
extension JXNavigationInteractiveTransition {
@objc func panGestureRecognizerAction(_ sender: UIPanGestureRecognizer) {
guard let view = sender.view else { return }
var progress = sender.translation(in: view).x / UIScreen.main.bounds.width
let velocity = sender.velocity(in: sender.view)
// pushpop
if sender.state == .began {
isGesturePush = velocity.x < 0 ? true : false
visibleVC = self.navigationController.visibleViewController
}
if isGesturePush {
progress = -progress
}
progress = min(1, max(0, progress))
// bcLog(message: progress)
switch sender.state {
case .began:
if self.isGesturePush {
if let visibleVC = self.visibleVC {
if visibleVC.jx_pushDelegate != nil {
self.interactionController = UIPercentDrivenInteractiveTransition()
visibleVC.jx_pushDelegate?.pushToNextViewController?()
self.pushScrollBegan()
}
}
} else {
self.interactionController = UIPercentDrivenInteractiveTransition()
self.navigationController.popViewController(animated: true)
self.popScrollBegan()
}
case .changed:
self.interactionController?.update(progress)
if self.isGesturePush {
self.pushScrollUpdate(progress: progress)
} else {
self.popScrollUpdate(progress: progress)
}
default:
var pushFinished: Bool = false
var finishProgress = 0.5
if self.isGesturePush {
finishProgress = 0.3
}
if progress > finishProgress {
pushFinished = true
self.interactionController?.finish()
} else {
pushFinished = false
self.interactionController?.cancel()
}
if self.isGesturePush {
pushScrollEnded(finished: pushFinished)
} else {
popScrollEnded(finished: pushFinished)
}
self.interactionController = nil
}
}
func pushScrollBegan() {
if let visibleVC = self.visibleVC {
visibleVC.jx_pushDelegate?.viewControllerPushScrollBegan?()
}
}
func pushScrollUpdate(progress: CGFloat) {
if let visibleVC = self.visibleVC {
visibleVC.jx_pushDelegate?.viewControllerPushScrollUpdate?(progress: progress)
}
}
func pushScrollEnded(finished: Bool) {
if let visibleVC = self.visibleVC {
visibleVC.jx_pushDelegate?.viewControllerPushScrollEnded?(finished: finished)
}
}
func popScrollBegan() {
if let visibleVC = self.visibleVC {
visibleVC.jx_popDelegate?.viewControllerPopScrollBegan?()
}
}
func popScrollUpdate(progress: CGFloat) {
if let visibleVC = self.visibleVC {
visibleVC.jx_popDelegate?.viewControllerPopScrollUpdate?(progress: progress)
}
}
func popScrollEnded(finished: Bool) {
if let visibleVC = self.visibleVC {
visibleVC.jx_popDelegate?.viewControllerPopScrollEnded?(finished: finished)
}
}
}
extension JXNavigationInteractiveTransition: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// ydLog(message: "========\(gestureRecognizer.jxTag ?? "")")
guard let jxTag = gestureRecognizer.jxTag else { return true }
//
if self.navigationController.jx_isTransitioning == true { return false }
// visibleViewController
guard let visibleVC = self.navigationController.visibleViewController else { return true }
//
if visibleVC.jx_popGestureType == .disabled { return false }
if let _ = gestureRecognizer as? UIScreenEdgePanGestureRecognizer {
if visibleVC.jx_popGestureType == .fullScreen { return false } //
// bug
if self.navigationController.viewControllers.count <= 1 { return false }
// ydLog(message: "========")
return true
} else if let panGesture = gestureRecognizer as? UIPanGestureRecognizer { //
// transition
let transition = panGesture.translation(in: gestureRecognizer.view)
if transition.x == 0 { return false }
if jxTag == "pop" {
if visibleVC.jx_popGestureType == .edge { return false }
if transition.x <= 0 { return false } //
if self.navigationController.viewControllers.count <= 1 { return false }
if visibleVC.jx_popDelegate?.viewControllerPopShouldScrollBegan?() == false { return false }
visibleVC.jx_popDelegate?.viewControllerPopScrollBegan?()
// ydLog(message: "========pop")
return true
} else if jxTag == "push" {
if transition.x >= 0 { return false } //
if visibleVC.jx_pushDelegate == nil { return false }
// ydLog(message: "========push")
return true
}
}
return true
}
}
extension JXNavigationInteractiveTransition: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if interactionController != nil, operation == .pop {
return JXPopAnimatedTransition()
} else if interactionController != nil, operation == .push {
return JXPushAnimatedTransition()
}
return nil
}
///
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if animationController.isKind(of: JXBaseAnimatedTransition.self) {
return interactionController
}
return nil
}
}

View File

@ -0,0 +1,72 @@
//
// JXPopAnimatedTransition.swift
// Test
//
// Created by on 2022/10/10.
//
import UIKit
class JXPopAnimatedTransition: JXBaseAnimatedTransition {
override func animateTransition() {
guard let fromView = self.fromViewController?.view else { return }
var toView = self.toViewController?.view
if toView == nil { return }
self.containerView?.insertSubview(toView!, belowSubview: fromView)
let size = self.containerView?.bounds.size ?? .zero
self.isHideTabBar = (self.toViewController?.tabBarController != nil) && (self.fromViewController?.hidesBottomBarWhenPushed == true) && (self.toViewController?.jx_captureImage != nil)
if self.isHideTabBar {
let captureView = UIImageView(image: self.toViewController?.jx_captureImage!)
captureView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
self.containerView?.insertSubview(captureView, belowSubview: fromView)
toView = captureView
self.toViewController?.view.isHidden = true
self.toViewController?.tabBarController?.tabBar.isHidden = true
}
self.contentView = toView
fromView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
toView!.frame = CGRect(x: -(0.3 * size.width), y: 0, width: size.width, height: size.height)
fromView.layer.shadowColor = UIColor.black.cgColor
fromView.layer.shadowOpacity = 0.15
fromView.layer.shadowRadius = 3.0
UIView.animate(withDuration: animationDuration(), delay: 0, options: .curveEaseInOut) {
fromView.frame = CGRect(x: size.width, y: 0, width: size.width, height: size.height)
if #available(iOS 11.0, *) {
toView!.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
}else {
toView!.transform = .identity
}
} completion: { finish in
if self.isHideTabBar {
if self.contentView != nil {
self.contentView?.removeFromSuperview()
self.contentView = nil
}
self.toViewController?.view.isHidden = false
if self.toViewController?.navigationController?.children.count == 1 {
self.toViewController?.tabBarController?.tabBar.isHidden = false
}
}
self.completeTransition()
}
}
}

View File

@ -0,0 +1,79 @@
//
// JXPushAnimatedTransition.swift
// Test
//
// Created by on 2022/10/10.
//
import UIKit
class JXPushAnimatedTransition: JXBaseAnimatedTransition {
override func animateTransition() {
guard let toView = self.toViewController?.view else { return }
var fromView = self.fromViewController?.view
let size = self.containerView?.bounds.size ?? .zero
// UITabBarControllerpush
self.isHideTabBar = (self.fromViewController?.tabBarController != nil) && (self.toViewController?.hidesBottomBarWhenPushed == true)
if self.isHideTabBar {
// fromViewController
let view: UIView?
if self.fromViewController?.view.window != nil {
view = self.fromViewController?.view.window
}else {
view = self.fromViewController?.view
}
if view != nil {
let captureImage = self.getCapture(with: view!)
let captureView = UIImageView(image: captureImage)
captureView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
containerView?.addSubview(captureView)
fromView = captureView
self.fromViewController?.jx_captureImage = captureImage
self.fromViewController?.view.isHidden = true
self.fromViewController?.tabBarController?.tabBar.isHidden = true
}
}
self.contentView = fromView
self.containerView?.addSubview(toView)
toView.frame = CGRect(x: size.width, y: 0, width: size.width, height: size.height)
toView.layer.shadowColor = UIColor.black.cgColor
toView.layer.shadowOpacity = 0.15
toView.layer.shadowRadius = 3.0
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseInOut) {
fromView?.frame = CGRect(x: -(0.3 * size.width), y: 0, width: size.width, height: size.height)
toView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
} completion: { finish in
if self.isHideTabBar {
if self.contentView != nil {
self.contentView!.removeFromSuperview()
self.contentView = nil
}
self.fromViewController?.view.isHidden = false
if self.fromViewController?.navigationController?.children.count == 1 {
self.fromViewController?.tabBarController?.tabBar.isHidden = false
}
}
self.completeTransition()
}
}
}

View File

@ -0,0 +1,47 @@
//
// JXTransitionDefine.swift
// Test
//
// Created by on 2022/10/11.
//
import UIKit
public func jx_swizzled_instanceMethod(_ prefix: String, oldClass: Swift.AnyClass!, oldSelector: String, newClass: Swift.AnyClass) {
let newSelector = prefix + "_" + oldSelector;
let originalSelector = NSSelectorFromString(oldSelector)
let swizzledSelector = NSSelectorFromString(newSelector)
let originalMethod = class_getInstanceMethod(oldClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(newClass, swizzledSelector)
let isAdd = class_addMethod(oldClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
if isAdd {
class_replaceMethod(newClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
}else {
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}
// MARK: - SwizzlingDispatchQueue.once线
extension DispatchQueue {
private static var onceTracker = [String]()
// Executes a block of code, associated with a unique token, only once. The code is thread safe and will only execute the code once even in the presence of multithreaded calls.
public class func once(token: String, block: () -> Void) {
// objc_sync_enter objc_sync_exit
objc_sync_enter(self)
defer { // defer
objc_sync_exit(self)
}
if onceTracker.contains(token) {
return
}
onceTracker.append(token)
block()
}
}

View File

@ -0,0 +1,16 @@
//
// JXTransitionDelegateBridge.swift
// Test
//
// Created by on 2022/10/10.
//
import UIKit
class JXTransitionDelegateBridge: NSObject {
weak var jx_pushDelegate: JXViewControllerPushDelegate?
weak var jx_popDelegate: JXViewControllerPopDelegate?
}

View File

@ -0,0 +1,25 @@
//
// UIGestureRecognizer+JXTransition.swift
// YDLive
//
// Created by on 2022/11/29.
//
import UIKit
extension UIGestureRecognizer {
fileprivate struct AssociatedKeys {
static var jxTag: Int?
}
var jxTag: String? {
get {
guard let obj = objc_getAssociatedObject(self, &AssociatedKeys.jxTag) as? String else { return nil }
return obj
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.jxTag, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

View File

@ -0,0 +1,130 @@
//
// UINavigationController+JXTransition.swift
// Test
//
// Created by on 2022/10/10.
//
import UIKit
extension UINavigationController {
/**
*/
private var gestureView: UIView? {
return self.interactivePopGestureRecognizer?.view
}
/**
*/
func jx_transitionAwake() {
// UIViewController.jxTransitionAwake()
self.delegate = self.interactiveTransition
self.interactivePopGestureRecognizer?.isEnabled = false
self.gestureView?.addGestureRecognizer(pushGesture)
self.gestureView?.addGestureRecognizer(popGesture)
self.gestureView?.addGestureRecognizer(edgePopGesture)
}
}
extension UINavigationController {
fileprivate struct AssociatedKeys {
static var screenPanGesture: Int?
static var panGesture: Int?
static var panPushGesture: Int?
static var transition: Int?
}
var interactiveTransition: JXNavigationInteractiveTransition {
get {
var transition = objc_getAssociatedObject(self, &AssociatedKeys.transition) as? JXNavigationInteractiveTransition
if transition == nil {
transition = JXNavigationInteractiveTransition()
transition?.navigationController = self
objc_setAssociatedObject(self, &AssociatedKeys.transition, transition, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return transition!
}
}
/**
*/
var edgePopGesture: UIScreenEdgePanGestureRecognizer {
get {
var panGesture = objc_getAssociatedObject(self, &AssociatedKeys.screenPanGesture) as? UIScreenEdgePanGestureRecognizer
if panGesture == nil {
panGesture = UIScreenEdgePanGestureRecognizer(target: self.systemTarget, action: self.systemAction)
panGesture?.jxTag = "edgePop"
panGesture?.edges = .left
panGesture?.delegate = self.interactiveTransition
objc_setAssociatedObject(self, &AssociatedKeys.screenPanGesture, panGesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return panGesture!
}
}
/**
*/
var popGesture: UIPanGestureRecognizer {
get {
var panGesture = objc_getAssociatedObject(self, &AssociatedKeys.panGesture) as? UIPanGestureRecognizer
if panGesture == nil {
panGesture = UIPanGestureRecognizer(target: self.systemTarget, action: self.systemAction)
panGesture?.jxTag = "pop"
panGesture?.maximumNumberOfTouches = 1
panGesture?.delegate = self.interactiveTransition
objc_setAssociatedObject(self, &AssociatedKeys.panGesture, panGesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return panGesture!
}
}
/**
push手势
*/
var pushGesture: UIPanGestureRecognizer {
get {
var panGesture = objc_getAssociatedObject(self, &AssociatedKeys.panPushGesture) as? UIPanGestureRecognizer
if panGesture == nil {
panGesture = UIPanGestureRecognizer(target: self.interactiveTransition, action: #selector(self.interactiveTransition.panGestureRecognizerAction(_:)))
panGesture?.jxTag = "push"
panGesture?.maximumNumberOfTouches = 1
panGesture?.delegate = self.interactiveTransition
objc_setAssociatedObject(self, &AssociatedKeys.panPushGesture, panGesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return panGesture!
}
}
var systemTarget: Any? {
get {
let internalTargets = self.interactivePopGestureRecognizer?.value(forKey: "targets") as? [AnyObject]
let internamTarget = internalTargets?.first?.value(forKey: "target")
return internamTarget
}
}
var systemAction: Selector {
return NSSelectorFromString("handleNavigationTransition:")
}
/**
*/
var jx_isTransitioning: Bool? {
return value(forKey: "_isTransitioning") as? Bool
}
}

View File

@ -0,0 +1,127 @@
//
// UIViewController+JXTransition.swift
// Test
//
// Created by on 2022/10/10.
//
import UIKit
// push
@objc public protocol JXViewControllerPushDelegate: NSObjectProtocol {
/// pushpush
@objc optional func pushToNextViewController()
/// push
@objc optional func viewControllerPushScrollBegan()
/// push
/// - Parameter progress: 0-1
@objc optional func viewControllerPushScrollUpdate(progress: CGFloat)
/// push
/// - Parameter finished: pushtruepush falsepush
@objc optional func viewControllerPushScrollEnded(finished: Bool)
}
// pop
@objc public protocol JXViewControllerPopDelegate: NSObjectProtocol {
@objc optional func viewControllerPopShouldScrollBegan() -> Bool
/// pop
@objc optional func viewControllerPopScrollBegan()
/// pop
/// - Parameter progress: 0-1
@objc optional func viewControllerPopScrollUpdate(progress: CGFloat)
/// pop
/// - Parameter finished: poptruepop falsepop
@objc optional func viewControllerPopScrollEnded(finished: Bool)
}
extension UIViewController {
// // MARK: -
// private static let onceToken = UUID().uuidString
// @objc public static func jxTransitionAwake() {
// DispatchQueue.once(token: onceToken) {
// let oriSels = ["viewDidAppear:",]
// for oriSel in oriSels {
// jx_swizzled_instanceMethod("jxGesture", oldClass: self, oldSelector: oriSel, newClass: self)
// }
// }
// }
//
// @objc func jxGesture_viewDidAppear(_ animated: Bool) {
// jxGesture_viewDidAppear(animated)
// }
}
extension UIViewController {
enum GestureTransitionType: Int {
///
case fullScreen
///
case edge
///
case disabled
}
fileprivate struct AssociatedKeys {
static var jxDelegateBridge: Int?
static var jx_gestureType: Int?
}
/**
*/
var jx_popGestureType: GestureTransitionType {
get {
guard let obj = objc_getAssociatedObject(self, &AssociatedKeys.jx_gestureType) as? GestureTransitionType else { return .fullScreen }
return obj
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.jx_gestureType, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
/**
*/
fileprivate var jx_delegateBridge: JXTransitionDelegateBridge {
get {
var bridge = objc_getAssociatedObject(self, &AssociatedKeys.jxDelegateBridge) as? JXTransitionDelegateBridge
if bridge == nil {
bridge = JXTransitionDelegateBridge()
objc_setAssociatedObject(self, &AssociatedKeys.jxDelegateBridge, bridge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return bridge!
}
}
var jx_pushDelegate: JXViewControllerPushDelegate? {
get {
return jx_delegateBridge.jx_pushDelegate
}
set {
jx_delegateBridge.jx_pushDelegate = newValue
}
}
var jx_popDelegate: JXViewControllerPopDelegate? {
get {
return jx_delegateBridge.jx_popDelegate
}
set {
jx_delegateBridge.jx_popDelegate = newValue
}
}
}

20
ShortPlay/Thirdparty/JXUUID/JXUUID.h vendored Normal file
View File

@ -0,0 +1,20 @@
//
// JXUUID.h
// 设备标识符
//
// Created by 曾觉新 on 2017/8/24.
// Copyright © 2017年 曾觉新. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface JXUUID : NSObject
+ (nonnull NSString *)uuid;
+ (nonnull NSString *)idfa;
/**
app后
*/
+ (nonnull NSString *)systemUUID;
@end

47
ShortPlay/Thirdparty/JXUUID/JXUUID.m vendored Normal file
View File

@ -0,0 +1,47 @@
//
// JXUUID.m
//
//
// Created by on 2017/8/24.
// Copyright © 2017 . All rights reserved.
//
#import "JXUUID.h"
#import <UIKit/UIKit.h>
#import "PDKeyChain.h"
#import <AdSupport/AdSupport.h>
static NSString *const uuidKey = @"com.JXUUID";
@implementation JXUUID
+ (nonnull NSString *)uuid
{
static NSString *uuid;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
uuid = [PDKeyChain objectForKey:uuidKey];
if (uuid && uuid.length > 0) {
} else {
uuid = [[NSUUID UUID] UUIDString];
[PDKeyChain setObject:uuid forKey:uuidKey];
}
});
return uuid;
}
+ (nonnull NSString *)idfa
{
static NSString *idfa;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
});
return idfa;
}
+ (nonnull NSString *)systemUUID
{
return [UIDevice currentDevice].identifierForVendor.UUIDString;
}
@end

31
ShortPlay/Thirdparty/JXUUID/PDKeyChain.h vendored Executable file
View File

@ -0,0 +1,31 @@
//
// PDKeyChain.h
// PDKeyChain
//
// Created by Panda on 16/8/23.
// Copyright © 2016年 v2panda. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
@interface PDKeyChain : NSObject
/**
* KeyChain
*
* @return NSDictionary
*/
+ (NSDictionary *)getKeyChainData;
+ (id)objectForKey:(NSString *)key;
+ (void)setObject:(id)object forKey:(NSString *)key;
+ (void)removeObjectForKey:(NSString *)key;
+ (void)removeAllObjects;
/**
* KeyChain
*/
+ (void)keyChainDelete;
@end

100
ShortPlay/Thirdparty/JXUUID/PDKeyChain.m vendored Executable file
View File

@ -0,0 +1,100 @@
//
// PDKeyChain.m
// PDKeyChain
//
// Created by Panda on 16/8/23.
// Copyright © 2016 v2panda. All rights reserved.
//
#import "PDKeyChain.h"
static NSString * const kPDKeyChainKey = @"com.shortplay.keychainKey";
@implementation PDKeyChain
+ (void)keyChainDelete{
[self delete:kPDKeyChainKey];
}
+ (NSDictionary *)getKeyChainData
{
NSDictionary *dic = [self load:kPDKeyChainKey];
if (!dic) {
dic = [NSDictionary dictionary];
}
return dic;
}
+ (void)setObject:(id)object forKey:(NSString *)key
{
NSMutableDictionary *tempDic = [[self getKeyChainData] mutableCopy];
[tempDic setObject:object forKey:key];
[self save:kPDKeyChainKey data:tempDic];
}
+ (id)objectForKey:(NSString *)key
{
NSDictionary *tempDic = [self getKeyChainData];
return tempDic[key];
}
+ (void)removeObjectForKey:(NSString *)key
{
NSMutableDictionary *tempDic = [[self getKeyChainData] mutableCopy];
[tempDic removeObjectForKey:key];
[self save:kPDKeyChainKey data:tempDic];
}
+ (void)removeAllObjects
{
NSMutableDictionary *tempDic = [[self getKeyChainData] mutableCopy];
[tempDic removeAllObjects];
[self save:kPDKeyChainKey data:tempDic];
}
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
return [NSMutableDictionary dictionaryWithObjectsAndKeys:
(id)kSecClassGenericPassword,(id)kSecClass,
service, (id)kSecAttrService,
service, (id)kSecAttrAccount,
(id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
nil];
}
+ (void)save:(NSString *)service data:(id)data {
//Get search dictionary
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Delete old item before add new item
SecItemDelete((CFDictionaryRef)keychainQuery);
//Add new object to search dictionary(Attention:the data format)
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
//Add item to keychain with the search dictionary
SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}
+ (id)load:(NSString *)service {
id ret = nil;
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Configure the search setting
//Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
@try {
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
} @catch (NSException *e) {
NSLog(@"Unarchive of %@ failed: %@", service, e);
} @finally {
}
}
if (keyData)
CFRelease(keyData);
return ret;
}
+ (void)delete:(NSString *)service {
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((CFDictionaryRef)keychainQuery);
}
@end

View File

@ -0,0 +1,127 @@
//
// JYPageConfig.swift
// JYPageController
//
// Created by wang tao on 2022/7/14.
//
import UIKit
@objc enum JYSegmentedItemState: Int {
case normal = 0
case selected = 1
}
@objc public enum JYSegmentedViewAlignment: Int {
case left
case right
case center
}
///IndicatorStyle
@objc public enum JYSegmentedViewIndicatorStyle: Int {
case none //. indicator hidden
case singleLine //线. default: lineWidth = title width
case customView //view,customIndicator. custom viewneed set customIndicator property
}
///refresh location
@objc public enum JYHeaderRefreshLocation: Int {
case headerViewTop //headerView. refresh headerView at headerView top
case childControllerViewTop //scrollView. refresh headerView at childControllerViewtop
}
public class JYPageConfig: NSObject {
public var itemBackgroundColor: UIColor?
public var itemBackgroundHeight: CGFloat = 0
public var itemBackgroundCornerRadius: CGFloat = 0
public var textMargin: CGFloat = 0.0
public var pageScrollBounces = false
///
public var hoverOffset: CGFloat = 0.0
///segmentItem. normal status title color, default black
public var normalTitleColor: UIColor = .black
///segmentItem. normal status title font, default size:16
public var normalTitleFont: CGFloat = 16
///segmentItem. normal status fontWeight, default regular
public var normalTitleFontWeight: UIFont.Weight = .regular
///segmentItem. selected status title color, default black
public var selectedTitleColor: UIColor = .black
///segmentItem. selected status title fontsize, default size:16
public var selectedTitleFont: CGFloat = 16
///segmentItem. selected status title fontWeight, default regular
public var selectedTitleFontWeight: UIFont.Weight = .regular
///segmentedView,线. indicator style, default singleLine
public var indicatorStyle: JYSegmentedViewIndicatorStyle = .singleLine
///segmentedViewview. custom indicatorView, if indicatorStyle = .customView, set this property
public var customIndicator: UIView?
///segmentedView. indicator width
public var indicatorWidth: CGFloat = 0
///segmentedView. indicator height
public var indicatorHeight: CGFloat = 2
///segmentedViewsegmentedView. indicator bottom distance from menuview bottomdefault 0
public var indicatorBottom: CGFloat = 0
///segmentedView. indicator color, if indicatorStyle =.customSizeLine || indicatorStyle =.followItemSizeLine, you can set this property
public var indicatorColor: UIColor = .red
///segmentedView. indicator cornerRadius, default 0
public var indicatorCornerRadius: CGFloat = 0
///线,false. indicator need sticky animation? default false
public var indicatorStickyAnimation: Bool = false
///segmentedView item. item margin
public var itemsMargin: CGFloat = 15
///segmentedView item. item min width, if text width < minwidth, item width = menuItemMinWidth
public var itemMinWidth: CGFloat = 0
///segmentedView item. item max width, if text width > maxwidth, item width = menuItemMaxWidth
public var itemMaxWidth: CGFloat = 0
///segmentedView itemsegmentedView. item top distance from meuuview top, default ver center
public var itemTop: CGFloat?
///segmentedView aligmentleft. default .left
public var alignment: JYSegmentedViewAlignment = .left
///segmentedView,0. segmentedView leftPadding
public var leftPadding: CGFloat = 0
///segmentedView,0. segmentedView rightPadding
public var rightPadding: CGFloat = 0
///segmentedView itemX,Y
///badgeViewOffSetdefault badgeView.left = item.right badgeView.centerY = item.top After you set badgeViewOffset, badgeView.left = item.right+offet.x, badgeView.centerY = item.top + offsetY
public var badgeViewOffset: CGPoint = .zero
///segmentedViewitemfalse. when the menuItem is clickedscrollView change to target page. need animation?
public var scrollViewAnimationWhenSegmentItemSelected: Bool = false
///segmentedView. segmentedView show in navigation bar, default false
public var segmentedViewShowInNavigationBar: Bool = false
///headerViewheaderView,headerView. when pageController has headerView, header refresh location. defalut: at headerView top
public var headerRefreshLocation: JYHeaderRefreshLocation = .headerViewTop
}

View File

@ -0,0 +1,21 @@
//
// JYPageContollerProtocol.swift
// JYPageController
//
// Created by wang tao on 2022/10/15.
//
import UIKit
@objc public protocol JYPageChildContollerProtocol where Self: UIViewController {
///fetch child controller scrollView. headerViewsegmentedViewtableView/collectionView/scrollView
@objc optional func fetchChildControllerScrollView() -> UIScrollView?
}
class JYPlaceHolderController: UIViewController,JYPageChildContollerProtocol {
}

View File

@ -0,0 +1,552 @@
//
// JYPageController.swift
// JYPageController
//
// Created by wang tao on 2022/7/14.
//
import UIKit
@objc public protocol JYPageControllerDataSource {
///segmentview frame
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect
///frame
func pageController(_ pageController: JYPageController, frameForContainerView container: UIScrollView) -> CGRect
///indexitemtitle
func pageController(_ pageController: JYPageController, titleAt index: Int) -> String
///indexitem
@objc optional func pageController(_ pageController: JYPageController, customViewAt index: Int) -> UIView?
///indexitembadgeView(eg. /frame.size)
@objc optional func pageController(_ pageController: JYPageController, badgeViewAt index: Int) -> UIView?
///
func numberOfChildControllers() -> Int
///indexUIViewController
func childController(atIndex index: Int) -> JYPageChildContollerProtocol
}
@objc public protocol JYPageControllerDelegate {
///childController
@objc optional func pageController(_ pageController: JYPageController, didLoadChildController: UIViewController, index: Int)
///scrollViewchildController
@objc optional func pageController(_ pageController: JYPageController, didEnterControllerAt index: Int)
@objc optional func pageController(_ pageController: JYPageController, mainDidScroll offsetY: CGFloat)
}
open class JYPageController: UIViewController {
///config
public var config: JYPageConfig = JYPageConfig.init()
///headerView
public var headerView: UIView? {
didSet {
if let header = headerView {
headerHeight = header.frame.size.height - self.config.hoverOffset
mainScrollView.tableHeaderView = header
}
}
}
///scrollView
public var scrollView: UIScrollView? {
get {
if headerView != nil {
return mainScrollView
}else {
return nil
}
}
}
///header view height
private var headerHeight: CGFloat = 0
///index
public var selectedIndex: Int = 0
///delegate
weak public var delegate: JYPageControllerDelegate?
///dataSource
weak public var dataSource: JYPageControllerDataSource?
///childViewController cache
private var childControllerCache: NSCache = NSCache<NSString, UIViewController>()
///scrollViewvcvc
private var displayControllerCache = Dictionary<NSString, UIViewController>()
///childController scrollView cache
private var childScrollViewCache: Dictionary = Dictionary<NSString, UIScrollView?>()
///menuview frame
private var menuViewFrame: CGRect = .zero
///container(scrollView)frame
private var childControllerViewFrame: CGRect = .zero
///scorllView
private var scrollByDragging = false
///
private var currentOffsetX: CGFloat = 0
///headerViewmenuView
private var scrollToTop: Bool = false
///scrollViewcontentOffsetY
private var verScrollViewContentOffsetY: CGFloat = 0
deinit {
childScrollViewCache.forEach { (key: NSString, value: UIScrollView?) in
value?.removeObserver(self, forKeyPath: "contentOffset")
}
}
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
delegate = self
dataSource = self
/**
init方法pageConfig,menuConfig的属性
eg.
config.showIndicatorLineView = false
config.selectedTitleColor = .red
config.normalTitleColor = .red
config.selectedTitleFont = .systemFont(ofSize: 18, weight: .medium)
config.normalTitleFont = .systemFont(ofSize: 18, weight: .medium)
config.menuItemMargin = 10
*/
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open override func viewDidLoad() {
super.viewDidLoad()
pageViewSetup()
segmentedView.select(selectedIndex)
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
mainScrollView.isScrollEnabled = headerView != nil
}
//MARK: - Public
///
public func reload() {
for i in 0 ..< childControllersCount {
let cacheKey = String(i) as NSString
if let childController = childControllerCache.object(forKey: cacheKey) {
childController.view.removeFromSuperview()
childController.willMove(toParent: nil)
childController.removeFromParent()
}
}
mainScrollView.removeFromSuperview()
childControllersCount = dataSource?.numberOfChildControllers() ?? 0
childControllerCache.removeAllObjects()
displayControllerCache.removeAll()
selectedIndex = 0
segmentedView.reload()
segmentedView.select(selectedIndex)
pageViewSetup()
pageContentScrollView.setContentOffset(CGPoint(x: CGFloat(selectedIndex) * childControllerViewFrame.width, y: 0), animated: false)
}
///menuviewscrollviewcontentsize
public func contentSizeForMenuView() -> CGSize {
return segmentedView.contentSize()
}
///menuviewframe
public func updateMenuViewFrame(frame: CGRect) {
menuViewFrame = frame
segmentedView.updateFrame(frame: frame)
}
///indexmenuItembadgeView
public func insertMenuItemBadgeView(_ badgeView: UIView, atIndex index: Int) {
segmentedView.addSegmentedItemBadgeView(badgeView, atIndex: index)
}
///indexmenuItembadgeView
public func removeMenuItemBadgeView(atIndex index: Int) {
segmentedView.removeSegmentedItemBadgeView(atIndex: index)
}
//MARK: - Private
private func pageViewSetup() {
guard let source = dataSource else {
return
}
pageContentScrollView.bounces = config.pageScrollBounces
menuViewFrame = source.pageController(self, frameForSegmentedView: segmentedView)
childControllerViewFrame = source.pageController(self, frameForContainerView: pageContentScrollView)
let mainScrollViewY : CGFloat = 0
// if let navBar = navigationController?.navigationBar {
// mainScrollViewY = navBar.frame.height + UIApplication.shared.statusBarFrame.size.height
// }
segmentedView.frame = menuViewFrame
pageContentScrollView.frame = childControllerViewFrame
mainScrollView.frame = CGRect(x: childControllerViewFrame.origin.x, y: mainScrollViewY, width: childControllerViewFrame.width, height: childControllerViewFrame.origin.y + childControllerViewFrame.height + self.config.hoverOffset)
// mainScrollView.frame = mainControllerViewFrame
let contentSize = CGSize(width: CGFloat(childControllersCount)*childControllerViewFrame.width, height: childControllerViewFrame.height)
pageContentScrollView.contentSize = contentSize
if config.segmentedViewShowInNavigationBar {
view.addSubview(pageContentScrollView)
navigationItem.titleView = segmentedView
}else {
view.addSubview(mainScrollView)
}
}
///indexcontroller
private func addChildController(index: Int) {
var childController = UIViewController()
let cacheKey = String(index) as NSString
if let controlller = childControllerCache.object(forKey: cacheKey) {
childController = controlller
}else {
if let controlller = dataSource?.childController(atIndex: index) {
if let childScrollView = controlller.fetchChildControllerScrollView?(),childScrollView.isKind(of: UIScrollView.classForCoder()) {
childScrollView.addObserver(self, forKeyPath: "contentOffset", options: [.old,.new], context: nil)
childScrollViewCache[cacheKey] = childScrollView
}
childControllerCache.setObject(controlller, forKey: cacheKey)
delegate?.pageController?(self, didLoadChildController: controlller, index: index)
childController = controlller
}
}
if displayControllerCache[cacheKey] == nil {
addChild(childController)
}
childController.view.frame = CGRect(x: CGFloat(index)*childControllerViewFrame.size.width, y: 0, width: childControllerViewFrame.size.width, height: childControllerViewFrame.size.height)
childController.didMove(toParent: self)
pageContentScrollView.addSubview(childController.view)
displayControllerCache[cacheKey] = childController
}
private func loadChildControllerIfNeeded() {
let offsetX = pageContentScrollView.contentOffset.x
if offsetX < 0 || offsetX > pageContentScrollView.contentSize.width {
return
}
guard offsetX.truncatingRemainder(dividingBy: childControllerViewFrame.size.width) == 0 else {
var targetIndex = 0
if offsetX > currentOffsetX {
targetIndex = Int(offsetX/childControllerViewFrame.size.width) + 1
}else {
targetIndex = Int(offsetX/childControllerViewFrame.size.width)
}
let cacheKey = String(targetIndex) as NSString
let controller = displayControllerCache[cacheKey]
if targetIndex < childControllersCount, controller == nil {
addChildController(index: targetIndex)
}
return
}
}
private func removeChildControllerIfNeeded() {
for i in 0 ..< childControllersCount {
let cacheKey = String(i) as NSString
if let childController = displayControllerCache[cacheKey], childControllerIsInScreen(childController) == false {
childController.view.removeFromSuperview()
childController.willMove(toParent: nil)
childController.removeFromParent()
displayControllerCache.removeValue(forKey: cacheKey)
}
}
}
private func childControllerIsInScreen(_ childController: UIViewController) -> Bool {
let offsetX = pageContentScrollView.contentOffset.x
let screenWidth = pageContentScrollView.frame.width
let childViewMaxX = childController.view.frame.maxX
let childViewMinX = childController.view.frame.minX
if childViewMaxX > offsetX, childViewMinX - offsetX < screenWidth {
return true
}else{
return false
}
}
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentOffset", headerHeight > 0 {
let cacheKey = String(selectedIndex) as NSString
//1.mainScrollViewcurrentChildListScrollViewsegmentedViewcurrentChildListScrollView
if let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint, newContentOffset.y > 0, mainScrollView.contentOffset.y < headerHeight {
let currentChildListScrollView = childScrollViewCache[cacheKey]
currentChildListScrollView??.contentOffset = .zero
}
//2.mainScrollViewscrollview
if config.headerRefreshLocation == .headerViewTop, let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint, newContentOffset.y < 0 {
let currentChildListScrollView = childScrollViewCache[cacheKey]
currentChildListScrollView??.contentOffset = .zero
}
//3.scrollView
if config.headerRefreshLocation == .childControllerViewTop {
//3.1segmentedViewheaderViewscrollview
if mainScrollView.contentOffset.y > 0, mainScrollView.contentOffset.y < headerHeight, let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint, newContentOffset.y < 0 {
let currentChildListScrollView = childScrollViewCache[cacheKey]
currentChildListScrollView??.contentOffset = .zero
}
}
}
}
//MARK: - Lazy
private lazy var childControllersCount: Int = {
return dataSource?.numberOfChildControllers() ?? 0
}()
private(set) lazy var segmentedView: JYSegmentedView = {
let segment = JYSegmentedView.init(pageConfig: config)
segment.dataSource = self
segment.delegate = self
return segment
}()
private lazy var pageContentScrollView : UIScrollView = {
let scrollView = UIScrollView()
// scrollView.backgroundColor = .white
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
scrollView.isPagingEnabled = true
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
}
return scrollView
}()
private lazy var mainScrollView : JYScrollView = {
let scrollView = JYScrollView(frame: .zero, style: .plain)
scrollView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
scrollView.backgroundColor = .clear
scrollView.showsVerticalScrollIndicator = false
scrollView.isScrollEnabled = false
scrollView.separatorStyle = .none
scrollView.delegate = self
scrollView.dataSource = self
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 15.0, *) {
scrollView.sectionHeaderTopPadding = 0
}
return scrollView
}()
}
//MARK: - UIScrollViewDelegate
extension JYPageController:UIScrollViewDelegate {
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView == pageContentScrollView {
currentOffsetX = scrollView.contentOffset.x
scrollByDragging = true
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == pageContentScrollView {
selectedIndex = Int(scrollView.contentOffset.x/scrollView.frame.width)
segmentedView.segmentedViewScrollEnd(byScrollEndDecelerating: scrollView)
delegate?.pageController?(self, didEnterControllerAt: selectedIndex)
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == pageContentScrollView {
removeChildControllerIfNeeded()
if scrollByDragging {
loadChildControllerIfNeeded()
segmentedView.segmentedViewScroll(by:scrollView)
currentOffsetX = scrollView.contentOffset.x
}
}
if scrollView == mainScrollView, headerView != nil,headerView?.frame.height ?? 0 > 0 {
let cacheKey = String(selectedIndex) as NSString
let currentChildListScrollView = childScrollViewCache[cacheKey]
//1.segmentedViewmainScrollView
if currentChildListScrollView??.contentOffset.y ?? 0 > 0 || scrollView.contentOffset.y >= headerHeight {
mainScrollView.contentOffset = CGPoint(x: 0, y: headerHeight)
}
//2.mainScrollView
if config.headerRefreshLocation == .childControllerViewTop, mainScrollView.contentOffset.y < 0 {
mainScrollView.contentOffset = .zero
}
//3.mainScrollView
if pageContentScrollView.contentOffset.x.truncatingRemainder(dividingBy: childControllerViewFrame.size.width) > 0, mainScrollView.contentOffset.y != verScrollViewContentOffsetY {
mainScrollView.setContentOffset(CGPoint(x: 0, y: verScrollViewContentOffsetY), animated: false)
}
verScrollViewContentOffsetY = mainScrollView.contentOffset.y
self.delegate?.pageController?(self, mainDidScroll: verScrollViewContentOffsetY)
}
}
}
//MARK: - JYPageControllerDelegate, JYPageControllerDataSource
extension JYPageController: JYPageControllerDelegate, JYPageControllerDataSource {
open func pageController(_ pageView: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {
return .zero
}
open func pageController(_ pageView: JYPageController, frameForContainerView container: UIScrollView) -> CGRect {
return .zero
}
open func pageController(_ pageView: JYPageController, titleAt index: Int) -> String {
return ""
}
open func pageController(_ pageController: JYPageController, customViewAt index: Int) -> UIView? {
return nil
}
open func pageController(_ pageView: JYPageController, badgeViewAt index: Int) -> UIView? {
return nil
}
open func numberOfChildControllers() -> Int {
return 0
}
open func childController(atIndex index: Int) -> JYPageChildContollerProtocol {
return JYPlaceHolderController()
}
open func pageController(_ pageController: JYPageController, didLoadChildController: UIViewController, index: Int) {
}
open func pageController(_ pageController: JYPageController, didEnterControllerAt index: Int) {
}
}
//MARK: - JYSegmentedViewDelegate,JYSegmentedViewDatasource
extension JYPageController: JYSegmentedViewDelegate, JYSegmentedViewDataSource {
public func numberOfSegmentedViewItems() -> Int {
return childControllersCount
}
public func segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int) -> String {
guard let source = dataSource else {
return ""
}
return source.pageController(self, titleAt: index)
}
public func segmentedView(_ segmentedView: JYSegmentedView, customViewAt index: Int) -> UIView? {
guard let source = dataSource else {
return nil
}
return source.pageController?(self, customViewAt: index)
}
public func segmentedView(_ segmentedView: JYSegmentedView, badgeViewAt index: Int) -> UIView? {
guard let source = dataSource else {
return nil
}
return source.pageController?(self, badgeViewAt: index)
}
public func segmentedView(_ segmentedView: JYSegmentedView, didSelectItemAt index: Int) {
scrollByDragging = false
let cacheKey = String(index) as NSString
let controller = displayControllerCache[cacheKey]
if index < childControllersCount {
if controller == nil {
addChildController(index: index)
}
selectedIndex = index
let contentOffsetX = CGFloat(index)*childControllerViewFrame.size.width
pageContentScrollView.setContentOffset(CGPoint(x: contentOffsetX, y: 0), animated: config.scrollViewAnimationWhenSegmentItemSelected)
delegate?.pageController?(self, didEnterControllerAt: index)
}
}
}
//MARK: - UITableViewDelegate,UITableViewDatasource
extension JYPageController: UITableViewDelegate, UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
// public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
// return menuViewFrame.height
// }
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// return pageContentScrollView.frame.height
// return self.view.frame.size.height
return mainScrollView.frame.height - self.config.hoverOffset
}
// public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// let headerContentView = UIView(frame: CGRect(x: 0, y: 0, width: menuViewFrame.size.width, height: menuViewFrame.size.height))
// headerContentView.backgroundColor = .white
// headerContentView.addSubview(segmentedView)
// return headerContentView
// }
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
cell.backgroundColor = .clear
cell.selectionStyle = .none
// pageContentScrollView.frame = CGRect(x: 0, y: 0, width: childControllerViewFrame.width, height: childControllerViewFrame.height)
cell.contentView.addSubview(segmentedView)
cell.contentView.addSubview(pageContentScrollView)
return cell
}
}

View File

@ -0,0 +1,18 @@
//
// JYScrollView.swift
// JYPageController
//
// Created by wang tao on 2022/10/12.
//
import UIKit
public class JYScrollView: UITableView,UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer.isKind(of: UIPanGestureRecognizer.classForCoder()) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.classForCoder())
}
}

View File

@ -0,0 +1,682 @@
//
// JYPageMenuView.swift
// JYPageController
//
// Created by wang tao on 2022/7/14.
//
import UIKit
@objc public protocol JYSegmentedViewDataSource {
///segmentedViewitems. segmentedView items count
func numberOfSegmentedViewItems() -> Int
///segmentedViewitem. segmentedView item title
func segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int) -> String
///segmentedViewitem,UIViewindextitle
/// index=1return button segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int)titleindex=1
///viewSize
///CustomView has higher priority than titlewhen return customView, ignore title. CustomView need set frame.size
@objc optional func segmentedView(_ segmentedView: JYSegmentedView, customViewAt index: Int) -> UIView?
///segmentedView item returnUIViewframe.size
///item badgeView (eg. label/red dot/icon, need set frame.size)
@objc optional func segmentedView(_ segmentedView: JYSegmentedView, badgeViewAt index: Int) -> UIView?
}
@objc public protocol JYSegmentedViewDelegate {
@objc optional func segmentedView(_ segmentedView: JYSegmentedView, didSelectItemAt index: Int)
}
public class JYSegmentedView: UIView {
/// config
var config: JYPageConfig = JYPageConfig()
/// delegate
weak public var delegate: JYSegmentedViewDelegate?
/// datasource
weak public var dataSource: JYSegmentedViewDataSource? {
didSet {
getItemsCount()
}
}
///index
private var selectedIndex: Int = 0
///item
private var itemsCount: Int = 0
///items
private var items = [JYSegmentedViewItem]()
///item
private var itemBackgroundViews = [UIImageView]()
///reloadlayoutSubviewlayoutItemsitemtransformtranformframe
var layoutOnceToken: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
addSubview(contentView)
contentView.addSubview(indicator)
}
public convenience init(pageConfig: JYPageConfig) {
self.init()
config = pageConfig
if config.indicatorStyle == .singleLine {
indicator.backgroundColor = config.indicatorColor
indicator.layer.cornerRadius = config.indicatorCornerRadius
}
if config.indicatorStyle == .none {
indicator.removeFromSuperview()
}
if config.indicatorStyle == .customView {
indicator.removeFromSuperview()
indicator = config.customIndicator ?? UIView()
contentView.addSubview(indicator)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
if newSuperview != nil {
reload()
}
}
public override func layoutSubviews() {
super.layoutSubviews()
contentView.frame = self.bounds
layoutItems()
indicatorMoveTo(index: selectedIndex, animate: false)
resetHorContentOffset(animate: false)
}
private func getItemsCount() {
guard let source = dataSource else {
return
}
itemsCount = source.numberOfSegmentedViewItems()
}
//MARK: - Public
///使segmentedView(reloadindexselect)
public func reload() {
guard let source = dataSource, source.numberOfSegmentedViewItems() > 0 else {
return
}
getItemsCount()
addItems()
layoutOnceToken = false
layoutItems()
resetHorContentOffset(animate: false)
indicatorMoveTo(index: selectedIndex, animate: false)
contentView.sendSubviewToBack(indicator)
itemBackgroundViews.forEach { contentView.sendSubviewToBack($0) }
}
///index
public func currentSelectedIndex() -> Int {
return selectedIndex
}
///segmentedviewcontentsize
public func contentSize() -> CGSize {
return contentView.contentSize
}
////segmentedviewframe
public func updateFrame(frame: CGRect) {
self.frame = frame
layoutIfNeeded()
resetHorContentOffset(animate: false)
}
///indexitembadgeView
public func addSegmentedItemBadgeView(_ badgeView: UIView, atIndex index: Int) {
guard let item = itemWithIndex(index) else {
return
}
if item.hasBadgeView {
item.badgeView?.removeFromSuperview()
}
item.badgeView = badgeView
contentView.addSubview(badgeView)
updateItemsFrame()
}
///indexitembadgeView
public func removeSegmentedItemBadgeView(atIndex index: Int) {
guard let menuItem = itemWithIndex(index) else {
return
}
if menuItem.hasBadgeView {
menuItem.badgeView?.removeFromSuperview()
menuItem.badgeView = nil
updateItemsFrame()
}
}
///
public func segmentedViewScroll(by pageView: UIScrollView) {
changeItemsByScrollViewDidScroll(scrollView: pageView)
updateItemsFrame()
}
///scrollVIewscrollEndDecelerating
public func segmentedViewScrollEnd(byScrollEndDecelerating pageView: UIScrollView) {
let offsetX = pageView.contentOffset.x
let currentIndex = Int(offsetX/pageView.frame.width)
selectedIndex = currentIndex
itemWithIndex(selectedIndex)?.selected = true
itemWithIndex(selectedIndex)?.font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)
for item in items {
if (item.tag - kMenuItemTagExtenValue) != selectedIndex {
item.selected = false
item.font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.normalTitleFontWeight)
}
}
resetHorContentOffset(animate: true)
indicatorMoveTo(index: selectedIndex, animate: true)
}
///index
public func select(_ index: Int) {
if index < itemsCount {
itemWithIndex(index)?.selected = true
selectedIndex = index
reload()
delegate?.segmentedView?(self, didSelectItemAt: selectedIndex)
}
}
//MARK: - Private
///item
private func changeItemsByScrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.frame.size.width <= 0 {
return
}
let offsetX = scrollView.contentOffset.x
let rate = offsetX.truncatingRemainder(dividingBy: scrollView.frame.size.width)/scrollView.frame.size.width
if rate <= 0 {
return
}
var selectedItem = itemWithIndex(selectedIndex)
var targetItem: JYSegmentedViewItem?
if offsetX > CGFloat(selectedIndex) * scrollView.frame.size.width {
let targetIndex = Int(offsetX/scrollView.frame.size.width) + 1
targetItem = itemWithIndex(targetIndex)
//:selectedIndexfromItemtoItem
if selectedIndex != Int(offsetX/scrollView.frame.size.width) {
itemWithIndex(selectedIndex)?.transform = .identity
if let item = targetItem {
item.transform = CGAffineTransform(scaleX:maxScale, y: maxScale)
}
selectedIndex = Int(offsetX/scrollView.frame.size.width)
selectedItem = itemWithIndex(selectedIndex)
resetHorContentOffset(animate: true)
updateItemColor()
}
}else{
let targetIndex = Int(offsetX/scrollView.frame.size.width)
targetItem = itemWithIndex(targetIndex)
//:selectedIndexfromItemtoItem
if selectedIndex != Int(offsetX/scrollView.frame.size.width) + 1 {
itemWithIndex(selectedIndex)?.transform = .identity
if let item = targetItem {
item.transform = CGAffineTransform(scaleX:maxScale , y: maxScale)
}
selectedIndex = Int(offsetX/scrollView.frame.size.width) + 1
selectedItem = itemWithIndex(selectedIndex)
resetHorContentOffset(animate: true)
updateItemColor()
}
}
guard let toItem = targetItem, let fromItem = selectedItem else {
return
}
if fromItem.tag < toItem.tag {
fromItem.rate = 1 - rate
toItem.rate = rate
let fromItemCurrentScaleX = maxScale - (maxScale - 1) * rate
let fromItemCurrentScaleY = maxScale - (maxScale - 1) * rate
let toItemCurrentScaleX = 1 + (maxScale - 1) * rate
let toItemCurrentScaleY = 1 + (maxScale - 1) * rate
fromItem.transform = CGAffineTransform(scaleX: fromItemCurrentScaleX, y: fromItemCurrentScaleY)
toItem.transform = CGAffineTransform(scaleX: toItemCurrentScaleX, y: toItemCurrentScaleY)
}else {
fromItem.rate = rate
toItem.rate = 1 - rate
let fromItemCurrentScaleX = maxScale - (maxScale - 1) * (1 - rate)
let fromItemCurrentScaleY = maxScale - (maxScale - 1) * (1 - rate)
let toItemCurrentScaleX = 1 + (maxScale - 1) * (1 - rate)
let toItemCurrentScaleY = 1 + (maxScale - 1) * (1 - rate)
fromItem.transform = CGAffineTransform(scaleX: fromItemCurrentScaleX, y: fromItemCurrentScaleY)
toItem.transform = CGAffineTransform(scaleX: toItemCurrentScaleX, y: toItemCurrentScaleY)
}
if config.indicatorStyle != .none {
indicatorMove(fromItem: fromItem, toItem: toItem, offsetX: offsetX, rate: rate, pageView: scrollView)
}
}
private func updateItemColor() {
items.forEach { item in
if item.tag == selectedIndex + kMenuItemTagExtenValue {
item.selected = true
}else{
item.selected = false
}
}
}
private func indicatorMove(fromItem: JYSegmentedViewItem, toItem: JYSegmentedViewItem, offsetX: CGFloat, rate: CGFloat, pageView: UIScrollView) {
let scrollViewWidth = pageView.frame.width
var currentIndicatorWidth: CGFloat = config.indicatorWidth > 0 ? config.indicatorWidth : fromItem.frame.width
let indicatorMaxWidth = abs(toItem.center.x - fromItem.center.x)
let tempOffetX = offsetX.truncatingRemainder(dividingBy: scrollViewWidth)
switch config.indicatorStyle {
case .singleLine:
if config.indicatorStickyAnimation {//线
if tempOffetX <= scrollViewWidth/2 {
let percent_min_max = tempOffetX/scrollViewWidth*2
currentIndicatorWidth = currentIndicatorWidth + percent_min_max * (indicatorMaxWidth - currentIndicatorWidth)
}else{
let percent_max_min = (tempOffetX - scrollViewWidth/2)/scrollViewWidth*2
currentIndicatorWidth = currentIndicatorWidth + (1 - percent_max_min)*(indicatorMaxWidth - currentIndicatorWidth)
}
if fromItem.tag < toItem.tag {
indicator.center = CGPoint(x: fromItem.center.x + rate * indicatorMaxWidth, y: indicator.center.y)
}else{
indicator.center = CGPoint(x: fromItem.center.x - (1 - rate) * indicatorMaxWidth, y: indicator.center.y)
}
var frame = indicator.frame
frame.size.width = currentIndicatorWidth
frame.size.height = indicator.frame.size.height
indicator.frame = frame
}else {
if fromItem.tag < toItem.tag {
indicator.center = CGPoint(x: fromItem.center.x + rate * indicatorMaxWidth, y: indicator.center.y)
}else{
indicator.center = CGPoint(x: fromItem.center.x - (1 - rate) * indicatorMaxWidth, y: indicator.center.y)
}
var frame = indicator.frame
frame.size.width = currentIndicatorWidth
frame.size.height = indicator.frame.size.height
indicator.frame = frame
}
case .customView:
if fromItem.tag < toItem.tag {
indicator.center = CGPoint(x: fromItem.center.x + rate * indicatorMaxWidth, y: indicator.center.y)
}else{
indicator.center = CGPoint(x: fromItem.center.x - (1 - rate) * indicatorMaxWidth, y: indicator.center.y)
}
default: break
}
}
private func addItems() {
guard let source = dataSource else{
return
}
for item in items {
item.removeFromSuperview()
item.badgeView?.removeFromSuperview()
}
items.removeAll()
itemBackgroundViews.forEach { $0.removeFromSuperview() }
itemBackgroundViews.removeAll()
for i in 0 ..< itemsCount {
let bgView = UIImageView()
bgView.layer.masksToBounds = true
contentView.addSubview(bgView)
itemBackgroundViews.append(bgView)
let customView = source.segmentedView?(self, customViewAt: i)
var item = JYSegmentedViewItem()
if let customItem = customView {
item = JYSegmentedViewItem(customItemView: customItem)
}else{
let title = source.segmentedView(self, titleAt: i)
item = JYSegmentedViewItem(text: title)
}
item.tag = i + kMenuItemTagExtenValue
item.config = config
item.delegate = self
contentView.addSubview(item)
items.append(item)
if let badgeView = source.segmentedView?(self, badgeViewAt: i) {
item.badgeView = badgeView
contentView.addSubview(badgeView)
}
}
}
///
private func indicatorMoveTo(index: Int, animate: Bool) {
guard let menuItem = itemWithIndex(selectedIndex), config.indicatorStyle != .none, itemsCount > 0 else {
return
}
var indicatorRect: CGRect = .zero
if config.indicatorStyle == .singleLine || config.indicatorStyle == .customView {
if config.indicatorWidth > 0 {//
indicatorRect = CGRect(x: (menuItem.frame.width - config.indicatorWidth)/2 + menuItem.frame.origin.x, y: frame.height - config.indicatorBottom - config.indicatorHeight, width: config.indicatorWidth, height: config.indicatorHeight)
}else {
indicatorRect = CGRect(x: CGRectGetMinX(menuItem.frame), y: frame.height - config.indicatorBottom - config.indicatorHeight, width: menuItem.frame.width, height: config.indicatorHeight)
}
}
// else if config.indicatorStyle == .customView {
// if let indicator = config.customIndicator {
// indicatorRect = CGRect(x: (menuItem.frame.width - indicator.frame.width)/2 + menuItem.frame.origin.x, y: frame.height - config.indicatorBottom - indicator.frame.height, width: indicator.frame.width, height: indicator.frame.height)
// }
// }
var duration: Double = 0
if animate {
duration = kMenuItemAnimateDuration
}
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut, animations: {
self.indicator.frame = indicatorRect
}, completion: nil)
}
///itemcontentOffsetX
private func resetHorContentOffset(animate: Bool) {
guard let selectedItem = itemWithIndex(selectedIndex) else {
return
}
let contentSize = contentView.contentSize
let width = contentView.frame.size.width
if contentSize.width > width {
//itemscrollView
let itemCenterX = selectedItem.center.x
if itemCenterX < width/2 {
contentView.setContentOffset(CGPoint(x: 0, y: 0), animated: animate)
}else{
if (contentSize.width - itemCenterX) > width/2 {
//item
let itemCenterByScreen = selectedItem.frame.origin.x - contentView.contentOffset.x + selectedItem.frame.size.width/2
let itemCenterToContentCenterDis = width/2 - itemCenterByScreen
contentView.setContentOffset(CGPoint(x:contentView.contentOffset.x - itemCenterToContentCenterDis, y: 0), animated: animate)
}else{
contentView.setContentOffset(CGPoint(x:contentSize.width - width, y: 0), animated: animate)
}
}
}
}
private func layoutItems() {
var totalWidth: CGFloat = 0
if frame.size.height > 1, frame.size.width > 1, layoutOnceToken == false {
for (index,item) in items.enumerated() {
//itemWidthmaxregular->medium medium->regular
let selectedItemWidth = sizeForItem(item, font: UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)).width
let normalItemWidth = sizeForItem(item, font: UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.normalTitleFontWeight)).width
var itemWidth = max(normalItemWidth,selectedItemWidth) + config.textMargin * 2
var itemHeight = sizeForItem(item, font: UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)).height
if item.type == .customView {
itemWidth = item.customView?.frame.size.width ?? 0
itemHeight = item.customView?.frame.size.height ?? 0
}
if index == 0 {
item.frame = CGRect(x: config.leftPadding, y: config.itemTop ?? (frame.size.height - itemHeight)/2, width: itemWidth, height: itemHeight)
totalWidth = totalWidth + itemWidth + config.leftPadding
}else{
item.frame = CGRect(x: totalWidth + config.itemsMargin, y:config.itemTop ?? (frame.size.height - itemHeight)/2, width: itemWidth, height: itemHeight)
totalWidth = totalWidth + itemWidth + config.itemsMargin
}
if index == selectedIndex {
item.transform = CGAffineTransform(scaleX: maxScale, y: maxScale)
item.textColor = config.selectedTitleColor
item.font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)
}else{
item.transform = .identity
item.font = normalFont
item.textColor = config.normalTitleColor
}
let bgView = self.itemBackgroundViews[index]
bgView.backgroundColor = config.itemBackgroundColor
if config.itemBackgroundHeight > 0 {
bgView.frame = CGRect(x: item.frame.origin.x, y: item.frame.origin.y, width: item.frame.size.width, height: config.itemBackgroundHeight)
} else {
bgView.frame = item.frame
}
bgView.center = item.center
bgView.layer.cornerRadius = config.itemBackgroundCornerRadius
}
///: item.transformframe, itemMargin, contentsize
updateItemsFrame()
layoutOnceToken = true
}
}
///itemsframe
private func updateItemsFrame() {
let width = calculateTotalWidth()
var startX: CGFloat = config.leftPadding
if config.alignment == .center, width < frame.width {
startX = (frame.width - width)/2 + config.leftPadding
}
if config.alignment == .right, width < frame.width {
startX = frame.width - width
}
var totalWidth: CGFloat = 0
for (index,item) in items.enumerated() {
if index == 0 {
item.frame = CGRect(x: startX , y: item.frame.origin.y, width: item.frame.width, height: item.frame.height)
item.badgeView?.frame = CGRect(x: item.frame.width + config.badgeViewOffset.x, y: item.frame.origin.y - item.badgeViewHeight/2 + config.badgeViewOffset.y, width: item.badgeViewWidth, height: item.badgeViewHeight)
totalWidth = startX + item.frame.width
}else{
item.frame = CGRect(x: totalWidth + config.itemsMargin , y: item.frame.origin.y, width: item.frame.width, height: item.frame.height)
item.badgeView?.frame = CGRect(x: item.frame.width + item.frame.origin.x + config.badgeViewOffset.x, y: item.frame.origin.y - item.badgeViewHeight/2 + config.badgeViewOffset.y, width: item.badgeViewWidth, height: item.badgeViewHeight)
totalWidth = totalWidth + item.frame.width + config.itemsMargin
}
let bgView = self.itemBackgroundViews[index]
bgView.backgroundColor = config.itemBackgroundColor
if config.itemBackgroundHeight > 0 {
bgView.frame = CGRect(x: item.frame.origin.x, y: item.frame.origin.y, width: item.frame.size.width, height: config.itemBackgroundHeight)
} else {
bgView.frame = item.frame
}
bgView.center = item.center
}
contentView.contentSize = CGSize(width: totalWidth + config.rightPadding, height: frame.size.height)
}
private func calculateTotalWidth() -> CGFloat {
var totalWidth: CGFloat = 0
for (index,item) in items.enumerated() {
if index == 0 {
totalWidth = totalWidth + item.frame.width + item.badgeViewWidth + config.badgeViewOffset.x
}else{
totalWidth = totalWidth + item.frame.width + item.badgeViewWidth + config.itemsMargin + config.badgeViewOffset.x
}
}
return totalWidth
}
private func itemWithIndex(_ index: Int) -> JYSegmentedViewItem? {
if let item = contentView.viewWithTag(index+kMenuItemTagExtenValue) as? JYSegmentedViewItem {
return item
}
return nil
}
//MARK: - Lazy
private lazy var contentView : UIScrollView = {
let scrollView = UIScrollView.init()
scrollView.showsHorizontalScrollIndicator = false
scrollView.bounces = false
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
}
return scrollView
}()
private lazy var indicator: UIView = {
let view = UIView()
view.backgroundColor = config.indicatorColor
view.layer.cornerRadius = config.indicatorCornerRadius
return view
}()
private lazy var maxScale: CGFloat = {
return config.selectedTitleFont/config.normalTitleFont
}()
private lazy var normalFont: UIFont = {
let font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.normalTitleFontWeight)
return font
}()
private lazy var selectedFont: UIFont = {
let font = UIFont.systemFont(ofSize: config.selectedTitleFont, weight: config.selectedTitleFontWeight)
return font
}()
//MARK: - Contant
private let kMenuItemTagExtenValue: Int = 1000000
private let kMenuItemAnimateDuration: Double = 0.3
}
//MARK: - JYSegmentedViewItemDelegate - itemextension
extension JYSegmentedView: JYSegmentedViewItemDelegate {
func segmentedItemDidSelected(_ item: JYSegmentedViewItem) {
let targetIndex = item.tag - kMenuItemTagExtenValue
segmentedViewSelectedItemChange(fromIndex: selectedIndex, toIndex: targetIndex)
selectedIndex = targetIndex
indicatorMoveTo(index: targetIndex, animate: true)
delegate?.segmentedView?(self, didSelectItemAt: targetIndex)
}
private func segmentedViewSelectedItemChange(fromIndex: Int, toIndex: Int) {
guard let fromItem = itemWithIndex(fromIndex), let toItem = itemWithIndex(toIndex) else {
return
}
toItem.selected = true
fromItem.selected = false
var rate: CGFloat = 0
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
timer.schedule(deadline: .now(), repeating: kMenuItemAnimateDuration/100)
timer.setEventHandler(handler: {
rate = rate + 0.01
fromItem.rate = 1 - rate
toItem.rate = rate
})
timer.resume()
UIView.animate(withDuration: kMenuItemAnimateDuration, delay: 0, options: .curveEaseInOut, animations: {
fromItem.textColor = self.config.normalTitleColor
toItem.textColor = self.config.selectedTitleColor
fromItem.transform = CGAffineTransform(scaleX: 1, y: 1)
toItem.transform = CGAffineTransform(scaleX: self.maxScale, y: self.maxScale)
self.updateItemsFrame()
}) { finished in
timer.cancel()
toItem.textColor = self.config.selectedTitleColor
toItem.font = UIFont.systemFont(ofSize: self.config.normalTitleFont, weight: self.config.selectedTitleFontWeight)
fromItem.textColor = self.config.normalTitleColor
fromItem.font = self.normalFont
self.resetHorContentOffset(animate: true)
}
}
///item
private func sizeForItem(_ item: JYSegmentedViewItem, font: UIFont) -> CGSize {
let attributes = [NSAttributedString.Key.font: font]
var titleSize: CGSize = NSString(string: item.text ?? "").boundingRect(with: CGSize.init(width: CGFloat(MAXFLOAT), height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: attributes, context: nil).size
if titleSize.width < config.itemMinWidth, config.itemMinWidth > 0 {
titleSize = CGSize(width: config.itemMinWidth, height: titleSize.height)
}
if titleSize.width > config.itemMaxWidth, config.itemMaxWidth > 0 {
titleSize = CGSize(width: config.itemMaxWidth, height: titleSize.height)
}
return titleSize
}
}

View File

@ -0,0 +1,146 @@
//
// JYPageMenuItem.swift
// JYPageController
//
// Created by wang tao on 2022/7/14.
//
import UIKit
@objc enum JYSegmentedViewItemType: Int {
case text
case customView
}
@objc protocol JYSegmentedViewItemDelegate {
func segmentedItemDidSelected(_ item: JYSegmentedViewItem)
}
class JYSegmentedViewItem: UIView {
weak open var delegate: JYSegmentedViewItemDelegate?
var badgeView: UIView?
var customView: UIView?
var type: JYSegmentedViewItemType = .text
var normalColorRed: CGFloat = 0, normalColorGreen: CGFloat = 0, normalColorBlue: CGFloat = 0, normalColorAlpha: CGFloat = 0
var selectedColorRed: CGFloat = 0, selectedColorGreen: CGFloat = 0, selectedColorBlue: CGFloat = 0, selectedColorAlpha: CGFloat = 0
var badgeViewWidth: CGFloat {
get {
return badgeView?.frame.width ?? 0
}
}
var badgeViewHeight: CGFloat {
get {
return badgeView?.frame.height ?? 0
}
}
var hasBadgeView: Bool {
get {
if badgeView == nil {
return false
}else{
return true
}
}
}
var selected: Bool = false {
didSet {
if selected {
textLabel.textColor = config.selectedTitleColor
}else{
textLabel.textColor = config.normalTitleColor
}
}
}
var font: UIFont? {
didSet {
textLabel.font = font
}
}
var textColor: UIColor? {
didSet {
textLabel.textColor = textColor
}
}
var text: String? {
get {
return textLabel.text
}
}
var config: JYPageConfig = JYPageConfig() {
didSet {
config.normalTitleColor.getRed(&normalColorRed, green: &normalColorGreen, blue: &normalColorBlue, alpha: &normalColorAlpha)
config.selectedTitleColor.getRed(&selectedColorRed, green: &selectedColorGreen, blue: &selectedColorBlue, alpha: &selectedColorAlpha)
}
}
var rate: CGFloat = 0 {
didSet {
let red: CGFloat = normalColorRed + (selectedColorRed - normalColorRed) * rate
let green: CGFloat = normalColorGreen + (selectedColorGreen - normalColorGreen) * rate
let blue: CGFloat = normalColorBlue + (selectedColorBlue - normalColorBlue) * rate
let alpha: CGFloat = normalColorAlpha + (selectedColorAlpha - normalColorAlpha) * rate
textLabel.textColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
let tap = UITapGestureRecognizer.init(target: self, action: #selector(tapAtion(_:)))
addGestureRecognizer(tap)
}
public convenience init(text: String) {
self.init(frame: .zero)
addSubview(textLabel)
textLabel.text = text
type = .text
}
public convenience init(customItemView: UIView) {
self.init(frame: .zero)
addSubview(customItemView)
customView = customItemView
type = .customView
}
override func layoutSubviews() {
super.layoutSubviews()
textLabel.frame = bounds
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Private
@objc private func tapAtion(_ gesture: UIGestureRecognizer) {
if !selected {
delegate?.segmentedItemDidSelected(self)
}
}
private lazy var textLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
return label
}()
}

View File

@ -0,0 +1,577 @@
//
// ZKCycleScrollView.swift
// ZKCycleScrollViewDemo
//
// Created by bestdew on 2019/3/8.
// Copyright © 2019 bestdew. All rights reserved.
//
// d*##$.
// zP"""""$e. $" $o
//4$ '$ $" $
//'$ '$ J$ $F
// 'b $k $> $
// $k $r J$ d$
// '$ $ $" $~
// '$ "$ '$E $
// $ $L $" $F ...
// $. 4B $ $$$*"""*b
// '$ $. $$ $$ $F
// "$ R$ $F $" $
// $k ?$ u* dF .$
// ^$. $$" z$ u$$$$e
// #$b $E.dW@e$" ?$
// #$ .o$$# d$$$$c ?F
// $ .d$$#" . zo$> #$r .uF
// $L .u$*" $&$$$k .$$d$$F
// $$" ""^"$$$P"$P9$
// JP .o$$$$u:$P $$
// $ ..ue$" "" $"
// d$ $F $
// $$ ....udE 4B
// #$ """"` $r @$
// ^$L '$ $F
// RN 4N $
// *$b d$
// $$k $F
// $$b $F
// $"" $F
// '$ $
// $L $
// '$ $
// $ $
import UIKit
public typealias ZKCycleScrollViewCell = UICollectionViewCell
public enum ZKScrollDirection: Int {
case horizontal
case vertical
}
@objc public protocol ZKCycleScrollViewDataSource: NSObjectProtocol {
/// Return number of pages
func numberOfItems(in cycleScrollView: ZKCycleScrollView) -> Int
/// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndex:
func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, cellForItemAt index: Int) -> ZKCycleScrollViewCell
}
@objc public protocol ZKCycleScrollViewDelegate: NSObjectProtocol {
/// Called when the cell is clicked
@objc optional func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, didSelectItemAt index: Int)
/// Called when the offset changes. The progress range is from 0 to the maximum index value, which means the progress value for a round of scrolling
@objc optional func cycleScrollViewDidScroll(_ cycleScrollView: ZKCycleScrollView, progress: Double)
/// Called when scrolling to a new index page
@objc optional func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, didScrollFromIndex fromIndex: Int, toIndex: Int)
}
@IBDesignable open class ZKCycleScrollView: UIView {
@IBOutlet open weak var delegate: ZKCycleScrollViewDelegate?
@IBOutlet open weak var dataSource: ZKCycleScrollViewDataSource?
var currentIdx = 0
var stepLength: CGFloat? //
#if TARGET_INTERFACE_BUILDER
@IBInspectable open var scrollDirection: Int = 0
#else
/// default horizontal. scroll direction
open var scrollDirection: ZKScrollDirection = .horizontal {
didSet {
switch scrollDirection {
case .vertical:
flowLayout?.scrollDirection = .vertical
default:
flowLayout?.scrollDirection = .horizontal
}
}
}
#endif
/// default 3.f. automatic scroll time interval
@IBInspectable open var autoScrollInterval: TimeInterval = 3 {
didSet {
addTimer()
}
}
@IBInspectable open var isAutoScroll: Bool = true {
didSet {
addTimer()
}
}
/// default true. turn off any dragging temporarily
@IBInspectable open var allowsDragging: Bool = true {
didSet {
collectionView.isScrollEnabled = allowsDragging
}
}
/// default the view size
@IBInspectable open var itemSize: CGSize = CGSize.zero {
didSet {
itemSizeFlag = true
flowLayout.itemSize = itemSize
flowLayout.headerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
flowLayout.footerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
}
}
/// default 0.0
@IBInspectable open var itemSpacing: CGFloat = 0.0 {
didSet {
flowLayout.minimumLineSpacing = itemSpacing
}
}
/// default 1.f(no scaling), it ranges from 0.f to 1.f
@IBInspectable open var itemZoomScale: CGFloat = 1.0 {
didSet {
flowLayout.zoomScale = itemZoomScale
}
}
@IBInspectable open var hidesPageControl: Bool = false {
didSet {
pageControl?.isHidden = hidesPageControl
}
}
@IBInspectable open var pageIndicatorTintColor: UIColor = UIColor.gray {
didSet {
pageControl?.pageIndicatorTintColor = pageIndicatorTintColor
}
}
@IBInspectable open var currentPageIndicatorTintColor: UIColor = UIColor.white {
didSet {
pageControl?.currentPageIndicatorTintColor = currentPageIndicatorTintColor
}
}
/// current page index
open var pageIndex: Int {
return changeIndex(currentIndex())
}
/// current content offset
open var contentOffset: CGPoint {
let num = CGFloat(numberOfAddedCells() / 2)
switch scrollDirection {
case .vertical:
return CGPoint(x: 0.0, y: max(0.0, collectionView.contentOffset.y - (flowLayout.itemSize.height + flowLayout.minimumLineSpacing) * num))
default:
return CGPoint(x: max(0.0, collectionView.contentOffset.x - (flowLayout.itemSize.width + flowLayout.minimumLineSpacing) * num), y: 0.0)
}
}
/// infinite cycle
@IBInspectable open private(set) var isInfiniteLoop: Bool = true
/// load completed callback
open var loadCompletion: (() -> Void)? = nil
open var pageControlFrame: CGRect?
var pageControl: UIPageControl!
var collectionView: UICollectionView!
private var flowLayout: ZKCycleScrollViewFlowLayout!
private var timer: Timer?
private var numberOfItems: Int = 0
private var fromIndex: Int = 0
private var itemSizeFlag: Bool = false
private var indexOffset: Int = 0
private var configuredFlag: Bool = false
private var tempIndex: Int = 0
// MARK: - Open Func
open func register(_ cellClass: AnyClass?, forCellWithReuseIdentifier identifier: String) {
collectionView.register(cellClass, forCellWithReuseIdentifier: identifier)
}
open func register(_ nib: UINib?, forCellWithReuseIdentifier identifier: String) {
collectionView.register(nib, forCellWithReuseIdentifier: identifier)
}
open func dequeueReusableCell(withReuseIdentifier identifier: String, for index: Int) -> ZKCycleScrollViewCell {
let indexPath = IndexPath(item: changeIndex(index), section: 0)
return collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
}
open func reloadData() {
removeTimer()
UIView.performWithoutAnimation {
self.collectionView.reloadData()
}
collectionView.performBatchUpdates(nil) { _ in
self.configuration()
self.loadCompletion?()
}
}
/// Call -beginUpdates and -endUpdates to update layout
/// Allows multiple scrollDirection/itemSize/itemSpacing/itemZoomScale to be set simultaneously.
open func beginUpdates() {
tempIndex = pageIndex
removeTimer()
}
open func endUpdates() {
flowLayout.invalidateLayout()
scrollToItem(at: tempIndex, animated: false)
addTimer()
}
/// Scroll to page
open func scrollToItem(at index: Int, animated: Bool) {
let num = numberOfAddedCells()
guard index >= 0 && index <= numberOfItems - 1 - num else {
print("attempt to scroll to invalid index:\(index)")
return
}
removeTimer()
let idx = index + num / 2
let position = scrollPosition()
let indexPath = IndexPath(item: idx, section: 0)
collectionView.scrollToItem(at: indexPath, at: position, animated: animated)
addTimer()
}
/// Returns the visible cell object at the specified index
open func cellForItem(at index: Int) -> ZKCycleScrollViewCell? {
let num = numberOfAddedCells()
guard index >= 0 && index <= numberOfItems - 1 - num else {
return nil
}
let idx = index + num / 2
let indexPath = IndexPath(item: idx, section: 0)
let cell = collectionView.cellForItem(at: indexPath)
return cell
}
// MARK: - Init
public init(frame: CGRect, shouldInfiniteLoop infiniteLoop: Bool? = nil) {
super.init(frame: frame)
isInfiniteLoop = infiniteLoop ?? true
initialization()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialization()
}
override open func layoutSubviews() {
super.layoutSubviews()
if itemSizeFlag {
flowLayout.itemSize = itemSize
flowLayout.headerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
flowLayout.footerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
} else {
flowLayout.itemSize = bounds.size
flowLayout.headerReferenceSize = CGSize.zero
flowLayout.footerReferenceSize = CGSize.zero
}
collectionView.frame = bounds
if let pageControlFrame = pageControlFrame {
pageControl.frame = pageControlFrame
} else {
pageControl.frame = CGRect(x: 0.0, y: bounds.height - 15.0, width: bounds.width, height: 15.0)
}
}
override open func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil { removeTimer() }
}
override open func setValue(_ value: Any?, forUndefinedKey key: String) {
guard key == "scrollDirection" else {
return;
}
let direction = value as! Int
if direction == 1 {
scrollDirection = .vertical
} else {
scrollDirection = .horizontal
}
}
deinit {
collectionView.delegate = nil
collectionView.dataSource = nil
}
// MARK: - Private Func
private func initialization() {
flowLayout = ZKCycleScrollViewFlowLayout()
flowLayout.minimumLineSpacing = 0
flowLayout.minimumInteritemSpacing = 0
flowLayout.scrollDirection = .horizontal
collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: flowLayout)
collectionView.backgroundColor = nil
collectionView.delegate = self
collectionView.dataSource = self
collectionView.scrollsToTop = false
collectionView.bounces = false
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
addSubview(collectionView)
pageControl = UIPageControl()
pageControl.isEnabled = false
pageControl.hidesForSinglePage = true
pageControl.pageIndicatorTintColor = UIColor.gray
pageControl.currentPageIndicatorTintColor = UIColor.white
addSubview(pageControl);
DispatchQueue.main.async {
self.configuration()
self.loadCompletion?()
}
}
private func configuration() {
fromIndex = 0
indexOffset = 0
configuredFlag = false
guard numberOfItems > 1 else { return }
let position = scrollPosition()
if isInfiniteLoop {
let indexPath = IndexPath(item: 2, section: 0)
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
}
addTimer()
updatePageControl()
configuredFlag = true
}
func addTimer() {
removeTimer()
if numberOfItems < 2 || !isAutoScroll || autoScrollInterval <= 0.0 { return }
timer = Timer.scheduledTimer(timeInterval: autoScrollInterval, target: YYWeakProxy(target: self), selector: #selector(automaticScroll), userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: .common)
}
private func updatePageControl() {
let num = numberOfAddedCells()
pageControl.currentPage = 0
pageControl.numberOfPages = max(0, numberOfItems - num)
pageControl.isHidden = (hidesPageControl || pageControl.numberOfPages < 2)
}
private func numberOfAddedCells() -> Int {
return isInfiniteLoop ? 4 : 0
}
@objc private func automaticScroll() {
var index = currentIndex() + 1
if !isInfiniteLoop && index >= numberOfItems {
index = 0
}
if let stepLength = stepLength {
let offsetX = self.collectionView.contentOffset.x
let width = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
self.collectionView.contentOffset = CGPoint(x: offsetX+stepLength, y: self.contentOffset.y)
let position = scrollPosition()
if self.currentIndex() == 1 {
let indexPath = IndexPath(item: numberOfItems - 3, section: 0)
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
} else if self.currentIndex() == numberOfItems - 2 {
let offsetx = (width)*CGFloat(2) - width*0.5
self.collectionView.contentOffset = CGPoint(x: offsetx, y: self.contentOffset.y)
self.collectionView.reloadData()
}
} else {
let position = scrollPosition()
let indexPath = IndexPath(item: index, section: 0)
collectionView.scrollToItem(at: indexPath, at: position, animated: true)
}
}
func removeTimer() {
timer?.invalidate()
timer = nil
}
private func scrollPosition() -> UICollectionView.ScrollPosition {
switch scrollDirection {
case .vertical:
return .centeredVertically
default:
return .centeredHorizontally
}
}
private func currentIndex() -> Int {
guard numberOfItems > 0 else {
return -1
}
var index = 0
var minimumIndex = 0
var maximumIndex = numberOfItems - 1
if numberOfItems == 1 {
return index
}
if isInfiniteLoop {
minimumIndex = 1
maximumIndex = numberOfItems - 2
}
switch scrollDirection {
case .vertical:
let height = flowLayout.itemSize.height + flowLayout.minimumLineSpacing
index = Int((collectionView.contentOffset.y + height / 2) / height)
default:
let width = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
index = Int((collectionView.contentOffset.x + width / 2) / width)
}
return min(maximumIndex, max(minimumIndex, index))
}
private func changeIndex(_ index: Int) -> Int {
guard isInfiniteLoop && numberOfItems > 1 else {
return index
}
var idx = index
if index == 0 {
idx = numberOfItems - 6
} else if index == 1 {
idx = numberOfItems - 5
} else if index == numberOfItems - 2 {
idx = 0
} else if index == numberOfItems - 1 {
idx = 1
} else {
idx = index - 2
}
return idx
}
}
extension ZKCycleScrollView: UICollectionViewDelegate {
public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
return true
}
public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didSelectItemAt:))) {
removeTimer()
}
}
public func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didSelectItemAt:))) {
addTimer()
}
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didSelectItemAt:))) {
delegate.cycleScrollView!(self, didSelectItemAt: changeIndex(indexPath.item))
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
pageControl.currentPage = pageIndex
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollViewDidScroll(_:progress:))) {
var total: CGFloat = 0.0
var offset: CGFloat = 0.0
let num = numberOfAddedCells()
switch scrollDirection {
case .vertical:
total = CGFloat(numberOfItems - 1 - num) * (flowLayout.itemSize.height + flowLayout.minimumLineSpacing)
offset = contentOffset.y.truncatingRemainder(dividingBy:((flowLayout.itemSize.height + flowLayout.minimumLineSpacing) * CGFloat(numberOfItems - num)))
default:
total = CGFloat(numberOfItems - 1 - num) * (flowLayout.itemSize.width + flowLayout.minimumLineSpacing)
offset = contentOffset.x.truncatingRemainder(dividingBy:((flowLayout.itemSize.width + flowLayout.minimumLineSpacing) * CGFloat(numberOfItems - num)))
}
let percent = Double(offset / total)
let progress = percent * Double(numberOfItems - 1 - num)
delegate.cycleScrollViewDidScroll!(self, progress: progress)
}
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
removeTimer()
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if let _ = stepLength {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.addTimer()
}
} else {
addTimer()
}
}
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
let index = currentIndex()
if isInfiniteLoop {
let position = scrollPosition()
if index == 1 {
let indexPath = IndexPath(item: numberOfItems - 3, section: 0)
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
} else if index == numberOfItems - 2 {
let indexPath = IndexPath(item: 2, section: 0)
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
}
}
let toIndex = changeIndex(index)
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didScrollFromIndex:toIndex:))) {
delegate.cycleScrollView!(self, didScrollFromIndex: fromIndex, toIndex: toIndex)
}
fromIndex = toIndex
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard pageIndex == fromIndex else { return }
let sum = velocity.x + velocity.y
if sum > 0 {
indexOffset = 1
} else if sum < 0 {
indexOffset = -1
} else {
indexOffset = 0
}
}
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
let position = scrollPosition()
var index = currentIndex() + indexOffset
index = max(0, index)
index = min(numberOfItems - 1, index)
let indexPath = IndexPath(item: index, section: 0)
collectionView.scrollToItem(at: indexPath, at: position, animated: true)
indexOffset = 0
}
}
extension ZKCycleScrollView: UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
numberOfItems = dataSource?.numberOfItems(in: self) ?? 0
if isInfiniteLoop && numberOfItems > 1 {
numberOfItems += numberOfAddedCells()
}
return numberOfItems
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let index = changeIndex(indexPath.item)
return (dataSource?.cycleScrollView(self, cellForItemAt: index))!
}
}

Some files were not shown because too many files have changed in this diff Show More