diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..6956a6f --- /dev/null +++ b/Podfile @@ -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 diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..cb053ea --- /dev/null +++ b/Podfile.lock @@ -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 diff --git a/ShortPlay.xcodeproj/project.pbxproj b/ShortPlay.xcodeproj/project.pbxproj new file mode 100644 index 0000000..488d0d9 --- /dev/null +++ b/ShortPlay.xcodeproj/project.pbxproj @@ -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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; +/* 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 = ""; + }; + 1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ShortPlayUITests; + sourceTree = ""; + }; + 1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 1DBC41052DA4F98D0093FCB0 /* Exceptions for "ShortPlay" folder in "ShortPlay" target */, + ); + path = ShortPlay; + sourceTree = ""; + }; +/* 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 = ""; + }; + 1DBC40502DA4EDFC0093FCB0 = { + isa = PBXGroup; + children = ( + 1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */, + 1DBC40722DA4EE010093FCB0 /* ShortPlayTests */, + 1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */, + 1DBC405A2DA4EDFC0093FCB0 /* Products */, + 0061C3496D158807460301A9 /* Pods */, + B6C9E282BAC4C4B3E926A853 /* Frameworks */, + ); + sourceTree = ""; + }; + 1DBC405A2DA4EDFC0093FCB0 /* Products */ = { + isa = PBXGroup; + children = ( + 1DBC40592DA4EDFC0093FCB0 /* ShortPlay.app */, + 1DBC406F2DA4EE010093FCB0 /* ShortPlayTests.xctest */, + 1DBC40792DA4EE010093FCB0 /* ShortPlayUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + B6C9E282BAC4C4B3E926A853 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3CB082E16E94AEEE7602DC87 /* Pods_ShortPlay.framework */, + 8DB457634938350C07D587D4 /* Pods_ShortPlay_ShortPlayUITests.framework */, + 3049C811FA3662418C9EEACE /* Pods_ShortPlayTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/ShortPlay.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ShortPlay.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ShortPlay.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ShortPlay.xcworkspace/contents.xcworkspacedata b/ShortPlay.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..da7100e --- /dev/null +++ b/ShortPlay.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ShortPlay.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ShortPlay.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..35d94ae --- /dev/null +++ b/ShortPlay.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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 +} diff --git a/ShortPlay/AppDelegate/AppDelegate+Config.swift b/ShortPlay/AppDelegate/AppDelegate+Config.swift new file mode 100644 index 0000000..e25c953 --- /dev/null +++ b/ShortPlay/AppDelegate/AppDelegate+Config.swift @@ -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 + } + + + } + +} diff --git a/ShortPlay/AppDelegate/AppDelegate.swift b/ShortPlay/AppDelegate/AppDelegate.swift new file mode 100644 index 0000000..3759846 --- /dev/null +++ b/ShortPlay/AppDelegate/AppDelegate.swift @@ -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) { + // 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. + } + + +} + diff --git a/ShortPlay/AppDelegate/SceneDelegate.swift b/ShortPlay/AppDelegate/SceneDelegate.swift new file mode 100644 index 0000000..8ea8e76 --- /dev/null +++ b/ShortPlay/AppDelegate/SceneDelegate.swift @@ -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. + } + + +} + diff --git a/ShortPlay/Base/Controller/SPNavigationController.swift b/ShortPlay/Base/Controller/SPNavigationController.swift new file mode 100644 index 0000000..d7bdcdc --- /dev/null +++ b/ShortPlay/Base/Controller/SPNavigationController.swift @@ -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) + } + +} diff --git a/ShortPlay/Base/Controller/SPTabBarController.swift b/ShortPlay/Base/Controller/SPTabBarController.swift new file mode 100644 index 0000000..bbfdd4e --- /dev/null +++ b/ShortPlay/Base/Controller/SPTabBarController.swift @@ -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 + } + +} diff --git a/ShortPlay/Base/Controller/SPViewController.swift b/ShortPlay/Base/Controller/SPViewController.swift new file mode 100644 index 0000000..7873c02 --- /dev/null +++ b/ShortPlay/Base/Controller/SPViewController.swift @@ -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) + } + } +} diff --git a/ShortPlay/Base/Define/SPDefine.swift b/ShortPlay/Base/Define/SPDefine.swift new file mode 100644 index 0000000..573126a --- /dev/null +++ b/ShortPlay/Base/Define/SPDefine.swift @@ -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 +} diff --git a/ShortPlay/Base/Define/SPUserDefaultsKey.swift b/ShortPlay/Base/Define/SPUserDefaultsKey.swift new file mode 100644 index 0000000..e52781c --- /dev/null +++ b/ShortPlay/Base/Define/SPUserDefaultsKey.swift @@ -0,0 +1,11 @@ +// +// SPUserDefaultsKey.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/9. +// + +import UIKit + +///登录的用户信息 +let kSPLoginTokenDefaultsKey = "kSPLoginTokenDefaultsKey" diff --git a/ShortPlay/Base/Extension/NSUserDefaults+JXAdd.h b/ShortPlay/Base/Extension/NSUserDefaults+JXAdd.h new file mode 100644 index 0000000..31076ae --- /dev/null +++ b/ShortPlay/Base/Extension/NSUserDefaults+JXAdd.h @@ -0,0 +1,21 @@ +// +// NSUserDefaults+JXAdd.h +// 链之家 +// +// Created by 曾觉新 on 2017/11/16. +// Copyright © 2017年 NetLoanHome. All rights reserved. +// + +#import + +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 diff --git a/ShortPlay/Base/Extension/NSUserDefaults+JXAdd.m b/ShortPlay/Base/Extension/NSUserDefaults+JXAdd.m new file mode 100644 index 0000000..b616067 --- /dev/null +++ b/ShortPlay/Base/Extension/NSUserDefaults+JXAdd.m @@ -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 diff --git a/ShortPlay/Base/Extension/String+SPAdd.swift b/ShortPlay/Base/Extension/String+SPAdd.swift new file mode 100644 index 0000000..3741755 --- /dev/null +++ b/ShortPlay/Base/Extension/String+SPAdd.swift @@ -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) + } + +} diff --git a/ShortPlay/Base/Extension/UIColor+SPAdd.swift b/ShortPlay/Base/Extension/UIColor+SPAdd.swift new file mode 100644 index 0000000..0204cc0 --- /dev/null +++ b/ShortPlay/Base/Extension/UIColor+SPAdd.swift @@ -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) + } +} + diff --git a/ShortPlay/Base/Extension/UIDevice+SPAdd.swift b/ShortPlay/Base/Extension/UIDevice+SPAdd.swift new file mode 100644 index 0000000..c6cc9ed --- /dev/null +++ b/ShortPlay/Base/Extension/UIDevice+SPAdd.swift @@ -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 + } + +} diff --git a/ShortPlay/Base/Extension/UIFont+SPAdd.swift b/ShortPlay/Base/Extension/UIFont+SPAdd.swift new file mode 100644 index 0000000..069b9d4 --- /dev/null +++ b/ShortPlay/Base/Extension/UIFont+SPAdd.swift @@ -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) + } +} diff --git a/ShortPlay/Base/Extension/UIImageView+SPAdd.swift b/ShortPlay/Base/Extension/UIImageView+SPAdd.swift new file mode 100644 index 0000000..7828639 --- /dev/null +++ b/ShortPlay/Base/Extension/UIImageView+SPAdd.swift @@ -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 + } + } + } +} diff --git a/ShortPlay/Base/Extension/UINavigationBar+SPAdd.swift b/ShortPlay/Base/Extension/UINavigationBar+SPAdd.swift new file mode 100644 index 0000000..266feaa --- /dev/null +++ b/ShortPlay/Base/Extension/UINavigationBar+SPAdd.swift @@ -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 + } + } +} + diff --git a/ShortPlay/Base/Extension/UINavigationController+SPAdd.swift b/ShortPlay/Base/Extension/UINavigationController+SPAdd.swift new file mode 100644 index 0000000..721e4ba --- /dev/null +++ b/ShortPlay/Base/Extension/UINavigationController+SPAdd.swift @@ -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 + } + +} diff --git a/ShortPlay/Base/Extension/UIView+SPAdd.swift b/ShortPlay/Base/Extension/UIView+SPAdd.swift new file mode 100644 index 0000000..a5a8d66 --- /dev/null +++ b/ShortPlay/Base/Extension/UIView+SPAdd.swift @@ -0,0 +1,15 @@ +// +// UIView+SPAdd.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/9. +// + +import UIKit +import SnapKit + +extension UIView { + + + +} diff --git a/ShortPlay/Base/Model/SPListModel.swift b/ShortPlay/Base/Model/SPListModel.swift new file mode 100644 index 0000000..3bc911f --- /dev/null +++ b/ShortPlay/Base/Model/SPListModel.swift @@ -0,0 +1,22 @@ +// +// SPListModel.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/9. +// + +import UIKit +import SmartCodable + +class SPListModel: 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? +} diff --git a/ShortPlay/Base/Model/SPModel.swift b/ShortPlay/Base/Model/SPModel.swift new file mode 100644 index 0000000..e9fe1ef --- /dev/null +++ b/ShortPlay/Base/Model/SPModel.swift @@ -0,0 +1,15 @@ +// +// SPModel.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/8. +// + +import UIKit +import SmartCodable + +class SPModel: NSObject { + required override init() { + + } +} diff --git a/ShortPlay/Base/Networking/API/SPHomeAPI.swift b/ShortPlay/Base/Networking/API/SPHomeAPI.swift new file mode 100644 index 0000000..5ee9098 --- /dev/null +++ b/ShortPlay/Base/Networking/API/SPHomeAPI.swift @@ -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?) -> Void)?) { + + var param = SPNetworkParameters(path: "/getRecommands") + param.method = .get + param.parameters = [ + "page_size" : 20, + "current_page" : page + ] + + SPNetwork.request(parameters: param) { (response: SPNetworkResponse>) in + completer?(response.data) + } + } + +} diff --git a/ShortPlay/Base/Networking/Base/SPApi.swift b/ShortPlay/Base/Networking/Base/SPApi.swift new file mode 100644 index 0000000..338b893 --- /dev/null +++ b/ShortPlay/Base/Networking/Base/SPApi.swift @@ -0,0 +1,117 @@ +// +// SPApi.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/8. +// + +import UIKit +import Moya +import SmartCodable + +struct SPNetworkData { + var parameters: SPNetworkParameters? + var completion: ((_ response: SPNetworkResponse) -> 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: 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 + } + } + +} + + diff --git a/ShortPlay/Base/Networking/Base/SPNetwork.swift b/ShortPlay/Base/Networking/Base/SPNetwork.swift new file mode 100644 index 0000000..1816b8a --- /dev/null +++ b/ShortPlay/Base/Networking/Base/SPNetwork.swift @@ -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(requestClosure: CustomApiTimeoutClosure) + + static func request(parameters: SPNetworkParameters, completion: ((_ response: SPNetworkResponse) -> 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(parameters: SPNetworkParameters, completion: ((_ response: SPNetworkResponse) -> 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(parameters: SPNetworkParameters, result: Result, completion: ((_ response: SPNetworkResponse) -> Void)?) { + + switch result { + case .success(let response): + let code = response.statusCode + if code == 401 || code == 402 || code == 403 { +// var data = SPNetworkData() +// data.parameters = parameters +// data.completion = completion +// +// awaitDataArr.append(data) +// awaitDataArr.first as? SPNetworkData + + + + 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.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() + response?.code = -1 + if parameters.isToast { + SPToast.show(text: "Error".localized) +// ETHUD.showToast(text: "系统错误".localized) + } + completion?(response!) + } + } catch { + var response = SPNetworkResponse() + 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() + 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.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 +} diff --git a/ShortPlay/Base/Networking/Base/SPURLPath.swift b/ShortPlay/Base/Networking/Base/SPURLPath.swift new file mode 100644 index 0000000..8e4f58b --- /dev/null +++ b/ShortPlay/Base/Networking/Base/SPURLPath.swift @@ -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 + + diff --git a/ShortPlay/Base/View/SPCollectionView.swift b/ShortPlay/Base/View/SPCollectionView.swift new file mode 100644 index 0000000..199a3f5 --- /dev/null +++ b/ShortPlay/Base/View/SPCollectionView.swift @@ -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") + } + +} diff --git a/ShortPlay/Base/View/SPCollectionViewCell.swift b/ShortPlay/Base/View/SPCollectionViewCell.swift new file mode 100644 index 0000000..5be1180 --- /dev/null +++ b/ShortPlay/Base/View/SPCollectionViewCell.swift @@ -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 + } +} diff --git a/ShortPlay/Base/View/SPImageView.swift b/ShortPlay/Base/View/SPImageView.swift new file mode 100644 index 0000000..8dabe83 --- /dev/null +++ b/ShortPlay/Base/View/SPImageView.swift @@ -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 + } + } + } + } + +} diff --git a/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift b/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift new file mode 100644 index 0000000..636894e --- /dev/null +++ b/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift @@ -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 + } + } + + } + +} diff --git a/ShortPlay/Class/Home/Controller/SPHomePageController.swift b/ShortPlay/Class/Home/Controller/SPHomePageController.swift new file mode 100644 index 0000000..5627976 --- /dev/null +++ b/ShortPlay/Class/Home/Controller/SPHomePageController.swift @@ -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 + } + + + +} diff --git a/ShortPlay/Class/Home/Controller/SPHomeViewController.swift b/ShortPlay/Class/Home/Controller/SPHomeViewController.swift new file mode 100644 index 0000000..33c6ee6 --- /dev/null +++ b/ShortPlay/Class/Home/Controller/SPHomeViewController.swift @@ -0,0 +1,21 @@ +// +// SPHomeViewController.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/8. +// + +import UIKit + +class SPHomeViewController: SPViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + + } + + + + +} diff --git a/ShortPlay/Class/Home/Model/SPHomeCategoryModel.swift b/ShortPlay/Class/Home/Model/SPHomeCategoryModel.swift new file mode 100644 index 0000000..bb63fb3 --- /dev/null +++ b/ShortPlay/Class/Home/Model/SPHomeCategoryModel.swift @@ -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? + +} diff --git a/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift b/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift new file mode 100644 index 0000000..ed05023 --- /dev/null +++ b/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift @@ -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() + } + + +} diff --git a/ShortPlay/Class/Player/Model/SPPlayerProtocol.swift b/ShortPlay/Class/Player/Model/SPPlayerProtocol.swift new file mode 100644 index 0000000..164af56 --- /dev/null +++ b/ShortPlay/Class/Player/Model/SPPlayerProtocol.swift @@ -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() + +} diff --git a/ShortPlay/Class/Player/Model/SPShortModel.swift b/ShortPlay/Class/Player/Model/SPShortModel.swift new file mode 100644 index 0000000..a86003b --- /dev/null +++ b/ShortPlay/Class/Player/Model/SPShortModel.swift @@ -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"] + ] + } +} diff --git a/ShortPlay/Class/Player/Model/SPVideoInfoModel.swift b/ShortPlay/Class/Player/Model/SPVideoInfoModel.swift new file mode 100644 index 0000000..4076825 --- /dev/null +++ b/ShortPlay/Class/Player/Model/SPVideoInfoModel.swift @@ -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? + +} diff --git a/ShortPlay/Class/Player/View/SPPlayerControlView.swift b/ShortPlay/Class/Player/View/SPPlayerControlView.swift new file mode 100644 index 0000000..b305ef4 --- /dev/null +++ b/ShortPlay/Class/Player/View/SPPlayerControlView.swift @@ -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) + } +} diff --git a/ShortPlay/Class/Player/View/SPPlayerListCell.swift b/ShortPlay/Class/Player/View/SPPlayerListCell.swift new file mode 100644 index 0000000..73f67b2 --- /dev/null +++ b/ShortPlay/Class/Player/View/SPPlayerListCell.swift @@ -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) { + + } + +} diff --git a/ShortPlay/Class/Player/View/SPPlayerProgressView.swift b/ShortPlay/Class/Player/View/SPPlayerProgressView.swift new file mode 100644 index 0000000..f538525 --- /dev/null +++ b/ShortPlay/Class/Player/View/SPPlayerProgressView.swift @@ -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) + } +} diff --git a/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift b/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift new file mode 100644 index 0000000..40da0b1 --- /dev/null +++ b/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift @@ -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 + } + } + + +} diff --git a/ShortPlay/Libs/APPTool/SPAPPTool.swift b/ShortPlay/Libs/APPTool/SPAPPTool.swift new file mode 100644 index 0000000..5786655 --- /dev/null +++ b/ShortPlay/Libs/APPTool/SPAPPTool.swift @@ -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() + } + } +} diff --git a/ShortPlay/Libs/HUD/SPToast.swift b/ShortPlay/Libs/HUD/SPToast.swift new file mode 100644 index 0000000..83a74d3 --- /dev/null +++ b/ShortPlay/Libs/HUD/SPToast.swift @@ -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) + } + +} diff --git a/ShortPlay/Libs/Login/SPLoginManager.swift b/ShortPlay/Libs/Login/SPLoginManager.swift new file mode 100644 index 0000000..a8c3af0 --- /dev/null +++ b/ShortPlay/Libs/Login/SPLoginManager.swift @@ -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) 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) +// } + } +} diff --git a/ShortPlay/Libs/Login/SPTokenModel.swift b/ShortPlay/Libs/Login/SPTokenModel.swift new file mode 100644 index 0000000..3d87689 --- /dev/null +++ b/ShortPlay/Libs/Login/SPTokenModel.swift @@ -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 + } +} diff --git a/ShortPlay/Libs/Player/SPPlayer.swift b/ShortPlay/Libs/Player/SPPlayer.swift new file mode 100644 index 0000000..d5a16c2 --- /dev/null +++ b/ShortPlay/Libs/Player/SPPlayer.swift @@ -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) + } + } + } + +} diff --git a/ShortPlay/Libs/SPLocalizedManager/SPLocalizedManager.swift b/ShortPlay/Libs/SPLocalizedManager/SPLocalizedManager.swift new file mode 100644 index 0000000..f4c7eeb --- /dev/null +++ b/ShortPlay/Libs/SPLocalizedManager/SPLocalizedManager.swift @@ -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) + } +} diff --git a/ShortPlay/Libs/User/SPUserInfo.swift b/ShortPlay/Libs/User/SPUserInfo.swift new file mode 100644 index 0000000..28b5479 --- /dev/null +++ b/ShortPlay/Libs/User/SPUserInfo.swift @@ -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 + } + +} diff --git a/ShortPlay/Source/Assets.xcassets/AccentColor.colorset/Contents.json b/ShortPlay/Source/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/AppIcon.appiconset/Contents.json b/ShortPlay/Source/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/Contents.json b/ShortPlay/Source/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json new file mode 100644 index 0000000..d77ddd3 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Contents.json new file mode 100644 index 0000000..6aa90f6 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@2x.png new file mode 100644 index 0000000..18132fe Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@3x.png new file mode 100644 index 0000000..d46c83d Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Contents.json new file mode 100644 index 0000000..6aa90f6 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@2x.png new file mode 100644 index 0000000..a086e35 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@3x.png new file mode 100644 index 0000000..4fdb930 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Contents.json new file mode 100644 index 0000000..d77ddd3 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Contents.json new file mode 100644 index 0000000..6aa90f6 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@2x.png new file mode 100644 index 0000000..8d492fa Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@3x.png new file mode 100644 index 0000000..78e9ca5 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03_selected.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03_selected.imageset/Contents.json new file mode 100644 index 0000000..d77ddd3 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03_selected.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Contents.json new file mode 100644 index 0000000..6aa90f6 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@2x.png new file mode 100644 index 0000000..24790e2 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@3x.png new file mode 100644 index 0000000..fbac8b6 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04_selected.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04_selected.imageset/Contents.json new file mode 100644 index 0000000..d77ddd3 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04_selected.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Contents.json new file mode 100644 index 0000000..d77ddd3 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Contents.json new file mode 100644 index 0000000..6aa90f6 --- /dev/null +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Contents.json @@ -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" + } +} diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@2x.png new file mode 100644 index 0000000..d2b8c94 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@2x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@3x.png new file mode 100644 index 0000000..d4c523a Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/Base.lproj/LaunchScreen.storyboard b/ShortPlay/Source/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/ShortPlay/Source/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ShortPlay/Source/Info.plist b/ShortPlay/Source/Info.plist new file mode 100644 index 0000000..7c5f0be --- /dev/null +++ b/ShortPlay/Source/Info.plist @@ -0,0 +1,25 @@ + + + + + ITSAppUsesNonExemptEncryption + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/ShortPlay/Source/ShortPlay-Bridging-Header.h b/ShortPlay/Source/ShortPlay-Bridging-Header.h new file mode 100644 index 0000000..a8edb74 --- /dev/null +++ b/ShortPlay/Source/ShortPlay-Bridging-Header.h @@ -0,0 +1,13 @@ +// +// ShortPlay-Bridging-Header.h +// ShortPlay +// +// Created by 曾觉新 on 2025/4/8. +// + +#import "JXUUID.h" +#import +#import +#import +#import "NSUserDefaults+JXAdd.h" +#import diff --git a/ShortPlay/Source/en.lproj/Localizable.strings b/ShortPlay/Source/en.lproj/Localizable.strings new file mode 100644 index 0000000..9de9a77 --- /dev/null +++ b/ShortPlay/Source/en.lproj/Localizable.strings @@ -0,0 +1,11 @@ +/* + Localizable.strings + ShortPlay + + Created by 曾觉新 on 2025/4/8. + 英语 +*/ + +"Home" = "Home"; +"For You" = "For You"; +"Error" = "Error"; diff --git a/ShortPlay/Thirdparty/JXTransition/JXBaseAnimatedTransition.swift b/ShortPlay/Thirdparty/JXTransition/JXBaseAnimatedTransition.swift new file mode 100644 index 0000000..7d64349 --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/JXBaseAnimatedTransition.swift @@ -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) + } + } +} diff --git a/ShortPlay/Thirdparty/JXTransition/JXNavigationInteractiveTransition.swift b/ShortPlay/Thirdparty/JXTransition/JXNavigationInteractiveTransition.swift new file mode 100644 index 0000000..eee2aab --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/JXNavigationInteractiveTransition.swift @@ -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) + + + // 在手势开始的时候判断是push操作还是pop操作 + 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 + } +} diff --git a/ShortPlay/Thirdparty/JXTransition/JXPopAnimatedTransition.swift b/ShortPlay/Thirdparty/JXTransition/JXPopAnimatedTransition.swift new file mode 100644 index 0000000..b0f0f74 --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/JXPopAnimatedTransition.swift @@ -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() + } + } + + +} diff --git a/ShortPlay/Thirdparty/JXTransition/JXPushAnimatedTransition.swift b/ShortPlay/Thirdparty/JXTransition/JXPushAnimatedTransition.swift new file mode 100644 index 0000000..24fd9ee --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/JXPushAnimatedTransition.swift @@ -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 + + // 解决UITabBarController左滑push时的显示问题 + 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() + } + } +} diff --git a/ShortPlay/Thirdparty/JXTransition/JXTransitionDefine.swift b/ShortPlay/Thirdparty/JXTransition/JXTransitionDefine.swift new file mode 100644 index 0000000..6889415 --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/JXTransitionDefine.swift @@ -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: - Swizzling会改变全局状态,所以用DispatchQueue.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() + } +} diff --git a/ShortPlay/Thirdparty/JXTransition/JXTransitionDelegateBridge.swift b/ShortPlay/Thirdparty/JXTransition/JXTransitionDelegateBridge.swift new file mode 100644 index 0000000..a938e4e --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/JXTransitionDelegateBridge.swift @@ -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? + +} diff --git a/ShortPlay/Thirdparty/JXTransition/UIGestureRecognizer+JXTransition.swift b/ShortPlay/Thirdparty/JXTransition/UIGestureRecognizer+JXTransition.swift new file mode 100644 index 0000000..0a49233 --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/UIGestureRecognizer+JXTransition.swift @@ -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) + } + } +} diff --git a/ShortPlay/Thirdparty/JXTransition/UINavigationController+JXTransition.swift b/ShortPlay/Thirdparty/JXTransition/UINavigationController+JXTransition.swift new file mode 100644 index 0000000..e02c4cf --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/UINavigationController+JXTransition.swift @@ -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 + } +} diff --git a/ShortPlay/Thirdparty/JXTransition/UIViewController+JXTransition.swift b/ShortPlay/Thirdparty/JXTransition/UIViewController+JXTransition.swift new file mode 100644 index 0000000..5a3eef8 --- /dev/null +++ b/ShortPlay/Thirdparty/JXTransition/UIViewController+JXTransition.swift @@ -0,0 +1,127 @@ +// +// UIViewController+JXTransition.swift +// Test +// +// Created by 曾觉新 on 2022/10/10. +// + +import UIKit + +// 左滑push代理 +@objc public protocol JXViewControllerPushDelegate: NSObjectProtocol { + /// 左滑push,在这里创建将要push的控制器 + @objc optional func pushToNextViewController() + + /// push手势滑动开始 + @objc optional func viewControllerPushScrollBegan() + + /// push手势滑动进度更新 + /// - Parameter progress: 进度(0-1) + @objc optional func viewControllerPushScrollUpdate(progress: CGFloat) + + /// push手势滑动结束 + /// - Parameter finished: 是否完成push操作(true:push成功 false:push取消) + @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: 是否完成pop操作(true:pop成功 false:pop取消) + @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 + } + } + +} + + + diff --git a/ShortPlay/Thirdparty/JXUUID/JXUUID.h b/ShortPlay/Thirdparty/JXUUID/JXUUID.h new file mode 100644 index 0000000..242e0b6 --- /dev/null +++ b/ShortPlay/Thirdparty/JXUUID/JXUUID.h @@ -0,0 +1,20 @@ +// +// JXUUID.h +// 设备标识符 +// +// Created by 曾觉新 on 2017/8/24. +// Copyright © 2017年 曾觉新. All rights reserved. +// + +#import + +@interface JXUUID : NSObject + ++ (nonnull NSString *)uuid; ++ (nonnull NSString *)idfa; +/** + 重新安装app后,会发生变化 + */ ++ (nonnull NSString *)systemUUID; + +@end diff --git a/ShortPlay/Thirdparty/JXUUID/JXUUID.m b/ShortPlay/Thirdparty/JXUUID/JXUUID.m new file mode 100644 index 0000000..be414ea --- /dev/null +++ b/ShortPlay/Thirdparty/JXUUID/JXUUID.m @@ -0,0 +1,47 @@ +// +// JXUUID.m +// 设备标识符 +// +// Created by 曾觉新 on 2017/8/24. +// Copyright © 2017年 曾觉新. All rights reserved. +// + +#import "JXUUID.h" +#import +#import "PDKeyChain.h" +#import + +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 diff --git a/ShortPlay/Thirdparty/JXUUID/PDKeyChain.h b/ShortPlay/Thirdparty/JXUUID/PDKeyChain.h new file mode 100755 index 0000000..45c8eb1 --- /dev/null +++ b/ShortPlay/Thirdparty/JXUUID/PDKeyChain.h @@ -0,0 +1,31 @@ +// +// PDKeyChain.h +// PDKeyChain +// +// Created by Panda on 16/8/23. +// Copyright © 2016年 v2panda. All rights reserved. +// + +#import +#import + +@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 diff --git a/ShortPlay/Thirdparty/JXUUID/PDKeyChain.m b/ShortPlay/Thirdparty/JXUUID/PDKeyChain.m new file mode 100755 index 0000000..68567dc --- /dev/null +++ b/ShortPlay/Thirdparty/JXUUID/PDKeyChain.m @@ -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 diff --git a/ShortPlay/Thirdparty/JYPageController/Classes/JYPageConfig.swift b/ShortPlay/Thirdparty/JYPageController/Classes/JYPageConfig.swift new file mode 100644 index 0000000..c5926be --- /dev/null +++ b/ShortPlay/Thirdparty/JYPageController/Classes/JYPageConfig.swift @@ -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 view,need 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 + + ///segmentedView自定义view指示器. 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 + + ///segmentedView指示器距离segmentedView底部间距. indicator bottom distance from menuview bottom,default 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 item距离segmentedView顶部的距离,默认垂直方向居中. item top distance from meuuview top, default ver center + public var itemTop: CGFloat? + + ///segmentedView aligment默认left. 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 item的角标位置X,Y方向调整 + ///badgeViewOffSet,default 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 + + ///点击segmentedView的item时候下面的子页面切换是否需要滚动动画,默认false. when the menuItem is clicked,scrollView change to target page. need animation? + public var scrollViewAnimationWhenSegmentItemSelected: Bool = false + + ///segmentedView是否显示在导航栏. segmentedView show in navigation bar, default false + public var segmentedViewShowInNavigationBar: Bool = false + + ///有headerView的时候下拉刷新位置在子页面的顶部,还是在headerView的顶部,默认在headerView的顶部. when pageController has headerView, header refresh location. defalut: at headerView top + public var headerRefreshLocation: JYHeaderRefreshLocation = .headerViewTop + +} + diff --git a/ShortPlay/Thirdparty/JYPageController/Classes/JYPageContollerProtocol.swift b/ShortPlay/Thirdparty/JYPageController/Classes/JYPageContollerProtocol.swift new file mode 100644 index 0000000..bc4a4f6 --- /dev/null +++ b/ShortPlay/Thirdparty/JYPageController/Classes/JYPageContollerProtocol.swift @@ -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. 有headerView的时候segmentedView需要悬浮的时候返回子页面中的tableView/collectionView/scrollView + @objc optional func fetchChildControllerScrollView() -> UIScrollView? + +} + + +class JYPlaceHolderController: UIViewController,JYPageChildContollerProtocol { + +} diff --git a/ShortPlay/Thirdparty/JYPageController/Classes/JYPageController.swift b/ShortPlay/Thirdparty/JYPageController/Classes/JYPageController.swift new file mode 100644 index 0000000..232709f --- /dev/null +++ b/ShortPlay/Thirdparty/JYPageController/Classes/JYPageController.swift @@ -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 + + ///第index位置上item的title + func pageController(_ pageController: JYPageController, titleAt index: Int) -> String + + ///第index位置上自定义item + @objc optional func pageController(_ pageController: JYPageController, customViewAt index: Int) -> UIView? + + ///第index位置上item右上角的badgeView(eg. 标签/小红点,必须设置frame.size) + @objc optional func pageController(_ pageController: JYPageController, badgeViewAt index: Int) -> UIView? + + ///子页面数量 + func numberOfChildControllers() -> Int + + ///返回第index位置上的UIViewController + func childController(atIndex index: Int) -> JYPageChildContollerProtocol +} + +@objc public protocol JYPageControllerDelegate { + + ///第一次加载childController调用 + @objc optional func pageController(_ pageController: JYPageController, didLoadChildController: UIViewController, index: Int) + + ///scrollView停止滚动,childController完全显示调用 + @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() + + ///缓存当前scrollView上展示的vc,用于处理子vc的生命周期逻辑 + private var displayControllerCache = Dictionary() + + ///childController scrollView cache + private var childScrollViewCache: Dictionary = Dictionary() + + ///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 + + ///有headerView的场景,记录menuView是都在顶部悬停 + private var scrollToTop: Bool = false + + ///竖直方向滚动的scrollView,contentOffsetY + 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) + } + + ///获取menuview中scrollview的contentsize + public func contentSizeForMenuView() -> CGSize { + return segmentedView.contentSize() + } + + ///更新menuview的frame + public func updateMenuViewFrame(frame: CGRect) { + menuViewFrame = frame + segmentedView.updateFrame(frame: frame) + } + + ///添加指定index的menuItem的badgeView + public func insertMenuItemBadgeView(_ badgeView: UIView, atIndex index: Int) { + segmentedView.addSegmentedItemBadgeView(badgeView, atIndex: index) + } + + ///移除指定index的menuItem的badgeView + 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) + } + } + + + ///添加指定index的controller + 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.处理mainScrollView和currentChildListScrollView的滚动冲突,向上滚动且segmentedView没有到悬浮位置之前,禁止currentChildListScrollView滚动 + if let newContentOffset = change?[NSKeyValueChangeKey.newKey] as? CGPoint, newContentOffset.y > 0, mainScrollView.contentOffset.y < headerHeight { + let currentChildListScrollView = childScrollViewCache[cacheKey] + currentChildListScrollView??.contentOffset = .zero + } + + //2.下拉刷新位置在mainScrollView顶部,控制子页面的scrollview不能向下弹性滚动 + 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.1处理segmentedView从悬浮状态下拉一直到headerView完全展示,整个过程中子页面的scrollview禁止下拉刷新 + 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.segmentedView悬浮的时候禁止mainScrollView滚动 + 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 + } +} + + + + diff --git a/ShortPlay/Thirdparty/JYPageController/Classes/JYScrollView.swift b/ShortPlay/Thirdparty/JYPageController/Classes/JYScrollView.swift new file mode 100644 index 0000000..f87c599 --- /dev/null +++ b/ShortPlay/Thirdparty/JYPageController/Classes/JYScrollView.swift @@ -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()) + } + +} + + diff --git a/ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedView.swift b/ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedView.swift new file mode 100644 index 0000000..11919ac --- /dev/null +++ b/ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedView.swift @@ -0,0 +1,682 @@ +// +// JYPageMenuView.swift +// JYPageController +// +// Created by wang tao on 2022/7/14. +// + +import UIKit + +@objc public protocol JYSegmentedViewDataSource { + + ///segmentedView中items数量. segmentedView items count + func numberOfSegmentedViewItems() -> Int + + ///segmentedView中item标题文案. segmentedView item title + func segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int) -> String + + ///segmentedView自定义item,实现了该方法且返回了UIView的话会忽略该index上的获取title的方法。 + ///例如 index=1的时候return button,那么 segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int)取title的时候会跳过index=1 + ///注意:返回的自定义view需要设置Size + ///CustomView has higher priority than title,when return customView, ignore title. CustomView need set frame.size + @objc optional func segmentedView(_ segmentedView: JYSegmentedView, customViewAt index: Int) -> UIView? + + ///segmentedView item 右上角的角标,return的UIView需要设置frame.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]() + + ///第一次初始化和reload的时候用来标记,layoutSubview中layoutItems方法只调用一次。原因:在对默认选中的item多次设置transform的时,tranform有值,但获取到的frame却没有变 + 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,动态改变数据源的时候调用(数据源改变之后先reload,如需要设置默认index,再调select)。其他场景不需要主动调用 + 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 + } + + ///获取segmentedview的contentsize + public func contentSize() -> CGSize { + return contentView.contentSize + } + + ///更新/设置segmentedview的frame + public func updateFrame(frame: CGRect) { + self.frame = frame + layoutIfNeeded() + resetHorContentOffset(animate: false) + } + + ///为第index位置上的item添加badgeView + 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() + } + + ///移除指定index的item的badgeView + 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() + } + + ///页面滚动停止,scrollVIew代理方法scrollEndDecelerating中调用 + 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) + + //备注:处理手指连续快速向左滑动时候,selectedIndex没有及时改变造成计算fromItem和toItem错乱问题 + 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) + + //备注:处理手指连续快速向右滑动时候,selectedIndex没有及时改变造成计算fromItem和toItem错乱问题 + 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) + } + + ///选中item之后判断是是否需要调整contentOffsetX + 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 { + //计算当前选中的item中心点是否超过scrollView宽度的一半 + 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() { + + //备注:itemWidth取max,解决无论字体从regular->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.transform之后更新frame, 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 - 选中item事件逻辑处理extension +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 + } + +} + + diff --git a/ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedViewItem.swift b/ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedViewItem.swift new file mode 100644 index 0000000..f9002e0 --- /dev/null +++ b/ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedViewItem.swift @@ -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 + }() + +} + diff --git a/ShortPlay/Thirdparty/ZKCycleScrollView-Swift/ZKCycleScrollView.swift b/ShortPlay/Thirdparty/ZKCycleScrollView-Swift/ZKCycleScrollView.swift new file mode 100644 index 0000000..267768c --- /dev/null +++ b/ShortPlay/Thirdparty/ZKCycleScrollView-Swift/ZKCycleScrollView.swift @@ -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) { + 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))! + } +} diff --git a/ShortPlay/Thirdparty/ZKCycleScrollView-Swift/ZKCycleScrollViewFlowLayout.swift b/ShortPlay/Thirdparty/ZKCycleScrollView-Swift/ZKCycleScrollViewFlowLayout.swift new file mode 100644 index 0000000..4d71f00 --- /dev/null +++ b/ShortPlay/Thirdparty/ZKCycleScrollView-Swift/ZKCycleScrollViewFlowLayout.swift @@ -0,0 +1,138 @@ +// +// ZKCycleScrollViewFlowLayout.swift +// ZKCycleScrollViewDemo +// +// Created by bestdew on 2019/3/22. +// 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 + +open class ZKCycleScrollViewFlowLayout: UICollectionViewFlowLayout { + + open var zoomScale: CGFloat = 1.0 + + override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + return true + } + + override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes: [UICollectionViewLayoutAttributes] = NSArray(array: super.layoutAttributesForElements(in: rect) ?? [], copyItems: true) as! [UICollectionViewLayoutAttributes] + if let collectionView = collectionView { + switch scrollDirection { + case .vertical: + let offset = collectionView.bounds.midY; + let distanceForScale = itemSize.height + minimumLineSpacing; + + for attr in attributes { + var scale: CGFloat = 0.0; + let distance = abs(offset - attr.center.y) + if distance >= distanceForScale { + scale = zoomScale; + } else if distance == 0.0 { + scale = 1.0 + attr.zIndex = 1 + } else { + scale = zoomScale + (distanceForScale - distance) * (1.0 - zoomScale) / distanceForScale + } + attr.transform = CGAffineTransform(scaleX: scale, y: scale) + } + default: + let offset = collectionView.bounds.midX; + let distanceForScale = itemSize.width + minimumLineSpacing; + + for attr in attributes { + var scale: CGFloat = 0.0; + let distance = abs(offset - attr.center.x) + if distance >= distanceForScale { + scale = zoomScale; + } else if distance == 0.0 { + scale = 1.0 + attr.zIndex = 1 + } else { + scale = zoomScale + (distanceForScale - distance) * (1.0 - zoomScale) / distanceForScale + } + attr.transform = CGAffineTransform(scaleX: scale, y: scale) + } + } + } + return attributes + } + + override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { + var point = proposedContentOffset + if let collectionView = collectionView { + switch scrollDirection { + case .vertical: + /// 计算出最终显示的矩形框 + let rect = CGRect(x: 0.0, y: point.y, width: collectionView.bounds.width, height: collectionView.bounds.height) + /// 计算collectionView最中心点的y值 + let centerY = point.y + collectionView.bounds.size.height * 0.5 + /// 存放最小的间距值 + var minDelta = CGFloat(MAXFLOAT) + /// 获得super已经计算好的布局属性 + if let attributes = super.layoutAttributesForElements(in: rect) { + for attr in attributes { + if abs(minDelta) > abs(attr.center.y - centerY) { + minDelta = attr.center.y - centerY + } + } + } + /// 修改原有的偏移量 + point.y += minDelta + default: + /// 计算出最终显示的矩形框 + let rect = CGRect(x: point.x, y: 0.0, width: collectionView.bounds.width, height: collectionView.bounds.height) + /// 计算collectionView最中心点的y值 + let centerX = point.x + collectionView.bounds.size.width * 0.5 + /// 存放最小的间距值 + var minDelta = CGFloat(MAXFLOAT) + /// 获得super已经计算好的布局属性 + if let attributes = super.layoutAttributesForElements(in: rect) { + for attr in attributes { + if abs(minDelta) > abs(attr.center.x - centerX) { + minDelta = attr.center.x - centerX + } + } + } + /// 修改原有的偏移量 + point.x += minDelta + } + } + return point + } +} diff --git a/ShortPlayTests/ShortPlayTests.swift b/ShortPlayTests/ShortPlayTests.swift new file mode 100644 index 0000000..44fbcff --- /dev/null +++ b/ShortPlayTests/ShortPlayTests.swift @@ -0,0 +1,17 @@ +// +// ShortPlayTests.swift +// ShortPlayTests +// +// Created by 曾觉新 on 2025/4/8. +// + +import Testing +@testable import ShortPlay + +struct ShortPlayTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/ShortPlayUITests/ShortPlayUITests.swift b/ShortPlayUITests/ShortPlayUITests.swift new file mode 100644 index 0000000..adb0c0f --- /dev/null +++ b/ShortPlayUITests/ShortPlayUITests.swift @@ -0,0 +1,43 @@ +// +// ShortPlayUITests.swift +// ShortPlayUITests +// +// Created by 曾觉新 on 2025/4/8. +// + +import XCTest + +final class ShortPlayUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/ShortPlayUITests/ShortPlayUITestsLaunchTests.swift b/ShortPlayUITests/ShortPlayUITestsLaunchTests.swift new file mode 100644 index 0000000..ed8b146 --- /dev/null +++ b/ShortPlayUITests/ShortPlayUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// ShortPlayUITestsLaunchTests.swift +// ShortPlayUITests +// +// Created by 曾觉新 on 2025/4/8. +// + +import XCTest + +final class ShortPlayUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}