app框架搭建,播放器开发
38
Podfile
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Uncomment the next line to define a global platform for your project
|
||||||
|
platform :ios, '13.0'
|
||||||
|
#source 'https://github.com/CocoaPods/Specs'
|
||||||
|
source 'https://cdn.cocoapods.org/'
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||||
|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
target 'ShortPlay' do
|
||||||
|
# Comment the next line if you don't want to use dynamic frameworks
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
pod 'Moya' #网络框架
|
||||||
|
pod 'SnapKit' #布局
|
||||||
|
pod 'SmartCodable' #数据解析
|
||||||
|
pod 'YYKit' #工具类
|
||||||
|
pod 'MJRefresh' #刷新控件
|
||||||
|
pod 'Toast' #吐司提示
|
||||||
|
pod 'ZFPlayer/AVPlayer' #播放器
|
||||||
|
pod 'KTVHTTPCache' #视频缓存
|
||||||
|
|
||||||
|
|
||||||
|
target 'ShortPlayTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
# Pods for testing
|
||||||
|
end
|
||||||
|
|
||||||
|
target 'ShortPlayUITests' do
|
||||||
|
# Pods for testing
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
58
Podfile.lock
Normal file
@ -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
|
761
ShortPlay.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,761 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 77;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1DBC41082DA4FC140093FCB0 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 1DBC41072DA4FC140093FCB0 /* Kingfisher */; };
|
||||||
|
44750E76352CA5F5653C130B /* Pods_ShortPlay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CB082E16E94AEEE7602DC87 /* Pods_ShortPlay.framework */; };
|
||||||
|
8259C195B8627F99A0DA2633 /* Pods_ShortPlayTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3049C811FA3662418C9EEACE /* Pods_ShortPlayTests.framework */; };
|
||||||
|
F30A67DA1559E80D2303D8BD /* Pods_ShortPlay_ShortPlayUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DB457634938350C07D587D4 /* Pods_ShortPlay_ShortPlayUITests.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
1DBC40702DA4EE010093FCB0 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 1DBC40512DA4EDFC0093FCB0 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 1DBC40582DA4EDFC0093FCB0;
|
||||||
|
remoteInfo = ShortPlay;
|
||||||
|
};
|
||||||
|
1DBC407A2DA4EE010093FCB0 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 1DBC40512DA4EDFC0093FCB0 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 1DBC40582DA4EDFC0093FCB0;
|
||||||
|
remoteInfo = ShortPlay;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1AC17585C566B70F526D3938 /* Pods-ShortPlay.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay.debug.xcconfig"; path = "Target Support Files/Pods-ShortPlay/Pods-ShortPlay.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
1B957067B5FAB12084843406 /* Pods-ShortPlay-ShortPlayUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay-ShortPlayUITests.release.xcconfig"; path = "Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
1DBC40592DA4EDFC0093FCB0 /* ShortPlay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShortPlay.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
1DBC406F2DA4EE010093FCB0 /* ShortPlayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShortPlayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
1DBC40792DA4EE010093FCB0 /* ShortPlayUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShortPlayUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
27D1AF89FD4CC591EFA405F2 /* Pods-ShortPlay-ShortPlayUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay-ShortPlayUITests.debug.xcconfig"; path = "Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
3049C811FA3662418C9EEACE /* Pods_ShortPlayTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShortPlayTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3CB082E16E94AEEE7602DC87 /* Pods_ShortPlay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShortPlay.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
7AA7065C6971326630D3239D /* Pods-ShortPlayTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlayTests.release.xcconfig"; path = "Target Support Files/Pods-ShortPlayTests/Pods-ShortPlayTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
8DB457634938350C07D587D4 /* Pods_ShortPlay_ShortPlayUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShortPlay_ShortPlayUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
9DF7FB1047F612A65429345F /* Pods-ShortPlay.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlay.release.xcconfig"; path = "Target Support Files/Pods-ShortPlay/Pods-ShortPlay.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
9E1BF460A8DF01704AD3DB63 /* Pods-ShortPlayTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShortPlayTests.debug.xcconfig"; path = "Target Support Files/Pods-ShortPlayTests/Pods-ShortPlayTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
1DBC41052DA4F98D0093FCB0 /* Exceptions for "ShortPlay" folder in "ShortPlay" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Source/Info.plist,
|
||||||
|
);
|
||||||
|
target = 1DBC40582DA4EDFC0093FCB0 /* ShortPlay */;
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
1DBC40722DA4EE010093FCB0 /* ShortPlayTests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = ShortPlayTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = ShortPlayUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
1DBC41052DA4F98D0093FCB0 /* Exceptions for "ShortPlay" folder in "ShortPlay" target */,
|
||||||
|
);
|
||||||
|
path = ShortPlay;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
1DBC40562DA4EDFC0093FCB0 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
1DBC41082DA4FC140093FCB0 /* Kingfisher in Frameworks */,
|
||||||
|
44750E76352CA5F5653C130B /* Pods_ShortPlay.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
1DBC406C2DA4EE010093FCB0 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
8259C195B8627F99A0DA2633 /* Pods_ShortPlayTests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
1DBC40762DA4EE010093FCB0 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
F30A67DA1559E80D2303D8BD /* Pods_ShortPlay_ShortPlayUITests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
0061C3496D158807460301A9 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1AC17585C566B70F526D3938 /* Pods-ShortPlay.debug.xcconfig */,
|
||||||
|
9DF7FB1047F612A65429345F /* Pods-ShortPlay.release.xcconfig */,
|
||||||
|
27D1AF89FD4CC591EFA405F2 /* Pods-ShortPlay-ShortPlayUITests.debug.xcconfig */,
|
||||||
|
1B957067B5FAB12084843406 /* Pods-ShortPlay-ShortPlayUITests.release.xcconfig */,
|
||||||
|
9E1BF460A8DF01704AD3DB63 /* Pods-ShortPlayTests.debug.xcconfig */,
|
||||||
|
7AA7065C6971326630D3239D /* Pods-ShortPlayTests.release.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1DBC40502DA4EDFC0093FCB0 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */,
|
||||||
|
1DBC40722DA4EE010093FCB0 /* ShortPlayTests */,
|
||||||
|
1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */,
|
||||||
|
1DBC405A2DA4EDFC0093FCB0 /* Products */,
|
||||||
|
0061C3496D158807460301A9 /* Pods */,
|
||||||
|
B6C9E282BAC4C4B3E926A853 /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
1DBC405A2DA4EDFC0093FCB0 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
1DBC40592DA4EDFC0093FCB0 /* ShortPlay.app */,
|
||||||
|
1DBC406F2DA4EE010093FCB0 /* ShortPlayTests.xctest */,
|
||||||
|
1DBC40792DA4EE010093FCB0 /* ShortPlayUITests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
B6C9E282BAC4C4B3E926A853 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3CB082E16E94AEEE7602DC87 /* Pods_ShortPlay.framework */,
|
||||||
|
8DB457634938350C07D587D4 /* Pods_ShortPlay_ShortPlayUITests.framework */,
|
||||||
|
3049C811FA3662418C9EEACE /* Pods_ShortPlayTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
1DBC40582DA4EDFC0093FCB0 /* ShortPlay */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 1DBC40822DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlay" */;
|
||||||
|
buildPhases = (
|
||||||
|
801A3E3FF53193556BBE9EBF /* [CP] Check Pods Manifest.lock */,
|
||||||
|
1DBC40552DA4EDFC0093FCB0 /* Sources */,
|
||||||
|
1DBC40562DA4EDFC0093FCB0 /* Frameworks */,
|
||||||
|
1DBC40572DA4EDFC0093FCB0 /* Resources */,
|
||||||
|
99BF4E2B3615B1F54D05DA28 /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
1DBC40FB2DA4F98D0093FCB0 /* ShortPlay */,
|
||||||
|
);
|
||||||
|
name = ShortPlay;
|
||||||
|
productName = ShortPlay;
|
||||||
|
productReference = 1DBC40592DA4EDFC0093FCB0 /* ShortPlay.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
1DBC406E2DA4EE010093FCB0 /* ShortPlayTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 1DBC40872DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
DD54CED5D8E8015BF138DA32 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
1DBC406B2DA4EE010093FCB0 /* Sources */,
|
||||||
|
1DBC406C2DA4EE010093FCB0 /* Frameworks */,
|
||||||
|
1DBC406D2DA4EE010093FCB0 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
1DBC40712DA4EE010093FCB0 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
1DBC40722DA4EE010093FCB0 /* ShortPlayTests */,
|
||||||
|
);
|
||||||
|
name = ShortPlayTests;
|
||||||
|
productName = ShortPlayTests;
|
||||||
|
productReference = 1DBC406F2DA4EE010093FCB0 /* ShortPlayTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
1DBC40782DA4EE010093FCB0 /* ShortPlayUITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 1DBC408A2DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayUITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
7A20F9056D18DF2605250323 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
1DBC40752DA4EE010093FCB0 /* Sources */,
|
||||||
|
1DBC40762DA4EE010093FCB0 /* Frameworks */,
|
||||||
|
1DBC40772DA4EE010093FCB0 /* Resources */,
|
||||||
|
36F6362AEACE4C584F0D7341 /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
1DBC407B2DA4EE010093FCB0 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
1DBC407C2DA4EE010093FCB0 /* ShortPlayUITests */,
|
||||||
|
);
|
||||||
|
name = ShortPlayUITests;
|
||||||
|
productName = ShortPlayUITests;
|
||||||
|
productReference = 1DBC40792DA4EE010093FCB0 /* ShortPlayUITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
1DBC40512DA4EDFC0093FCB0 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = 1;
|
||||||
|
LastSwiftUpdateCheck = 1620;
|
||||||
|
LastUpgradeCheck = 1620;
|
||||||
|
TargetAttributes = {
|
||||||
|
1DBC40582DA4EDFC0093FCB0 = {
|
||||||
|
CreatedOnToolsVersion = 16.2;
|
||||||
|
};
|
||||||
|
1DBC406E2DA4EE010093FCB0 = {
|
||||||
|
CreatedOnToolsVersion = 16.2;
|
||||||
|
TestTargetID = 1DBC40582DA4EDFC0093FCB0;
|
||||||
|
};
|
||||||
|
1DBC40782DA4EE010093FCB0 = {
|
||||||
|
CreatedOnToolsVersion = 16.2;
|
||||||
|
TestTargetID = 1DBC40582DA4EDFC0093FCB0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 1DBC40542DA4EDFC0093FCB0 /* Build configuration list for PBXProject "ShortPlay" */;
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 1DBC40502DA4EDFC0093FCB0;
|
||||||
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
1DBC41062DA4FC140093FCB0 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||||
|
);
|
||||||
|
preferredProjectObjectVersion = 77;
|
||||||
|
productRefGroup = 1DBC405A2DA4EDFC0093FCB0 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
1DBC40582DA4EDFC0093FCB0 /* ShortPlay */,
|
||||||
|
1DBC406E2DA4EE010093FCB0 /* ShortPlayTests */,
|
||||||
|
1DBC40782DA4EE010093FCB0 /* ShortPlayUITests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
1DBC40572DA4EDFC0093FCB0 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
1DBC406D2DA4EE010093FCB0 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
1DBC40772DA4EE010093FCB0 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
36F6362AEACE4C584F0D7341 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ShortPlay-ShortPlayUITests/Pods-ShortPlay-ShortPlayUITests-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
7A20F9056D18DF2605250323 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-ShortPlay-ShortPlayUITests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
801A3E3FF53193556BBE9EBF /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-ShortPlay-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
99BF4E2B3615B1F54D05DA28 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay/Pods-ShortPlay-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-ShortPlay/Pods-ShortPlay-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ShortPlay/Pods-ShortPlay-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
DD54CED5D8E8015BF138DA32 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-ShortPlayTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
1DBC40552DA4EDFC0093FCB0 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
1DBC406B2DA4EE010093FCB0 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
1DBC40752DA4EE010093FCB0 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
1DBC40712DA4EE010093FCB0 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 1DBC40582DA4EDFC0093FCB0 /* ShortPlay */;
|
||||||
|
targetProxy = 1DBC40702DA4EE010093FCB0 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
1DBC407B2DA4EE010093FCB0 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 1DBC40582DA4EDFC0093FCB0 /* ShortPlay */;
|
||||||
|
targetProxy = 1DBC407A2DA4EE010093FCB0 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
1DBC40832DA4EE010093FCB0 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1AC17585C566B70F526D3938 /* Pods-ShortPlay.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 36QJHAN62Q;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = ShortPlay/Source/Info.plist;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
|
INFOPLIST_KEY_UIMainStoryboardFile = "";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlay;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "ShortPlay/Source/ShortPlay-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
1DBC40842DA4EE010093FCB0 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9DF7FB1047F612A65429345F /* Pods-ShortPlay.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 36QJHAN62Q;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = ShortPlay/Source/Info.plist;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
|
INFOPLIST_KEY_UIMainStoryboardFile = "";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlay;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "ShortPlay/Source/ShortPlay-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
1DBC40852DA4EE010093FCB0 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
1DBC40862DA4EE010093FCB0 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
1DBC40882DA4EE010093FCB0 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9E1BF460A8DF01704AD3DB63 /* Pods-ShortPlayTests.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 36QJHAN62Q;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ShortPlay.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ShortPlay";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
1DBC40892DA4EE010093FCB0 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AA7065C6971326630D3239D /* Pods-ShortPlayTests.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 36QJHAN62Q;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ShortPlay.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ShortPlay";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
1DBC408B2DA4EE010093FCB0 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 27D1AF89FD4CC591EFA405F2 /* Pods-ShortPlay-ShortPlayUITests.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 36QJHAN62Q;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayUITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = ShortPlay;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
1DBC408C2DA4EE010093FCB0 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1B957067B5FAB12084843406 /* Pods-ShortPlay-ShortPlayUITests.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 36QJHAN62Q;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = cn.com.boytv.ShortPlayUITests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_TARGET_NAME = ShortPlay;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
1DBC40542DA4EDFC0093FCB0 /* Build configuration list for PBXProject "ShortPlay" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
1DBC40852DA4EE010093FCB0 /* Debug */,
|
||||||
|
1DBC40862DA4EE010093FCB0 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
1DBC40822DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlay" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
1DBC40832DA4EE010093FCB0 /* Debug */,
|
||||||
|
1DBC40842DA4EE010093FCB0 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
1DBC40872DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
1DBC40882DA4EE010093FCB0 /* Debug */,
|
||||||
|
1DBC40892DA4EE010093FCB0 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
1DBC408A2DA4EE010093FCB0 /* Build configuration list for PBXNativeTarget "ShortPlayUITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
1DBC408B2DA4EE010093FCB0 /* Debug */,
|
||||||
|
1DBC408C2DA4EE010093FCB0 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
1DBC41062DA4FC140093FCB0 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 8.3.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
1DBC41072DA4FC140093FCB0 /* Kingfisher */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 1DBC41062DA4FC140093FCB0 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||||
|
productName = Kingfisher;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
};
|
||||||
|
rootObject = 1DBC40512DA4EDFC0093FCB0 /* Project object */;
|
||||||
|
}
|
7
ShortPlay.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
10
ShortPlay.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:ShortPlay.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
15
ShortPlay.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "917a2b512148612347b307b5f8d624e74df48af36fa322c80901a86fc0e1317f",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "kingfisher",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/onevcat/Kingfisher.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
|
||||||
|
"version" : "8.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
73
ShortPlay/AppDelegate/AppDelegate+Config.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate+Config.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension AppDelegate {
|
||||||
|
|
||||||
|
func appConfig() {
|
||||||
|
// UIView.et_Awake()
|
||||||
|
tabBarConfig()
|
||||||
|
// keyBoardStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppDelegate {
|
||||||
|
///配置tabBar样式
|
||||||
|
private func tabBarConfig() {
|
||||||
|
|
||||||
|
let tabBar = UITabBar.appearance();
|
||||||
|
|
||||||
|
|
||||||
|
let font = UIFont.fontRegular(ofSize: 10)
|
||||||
|
let normalColor = UIColor.color7F7F80()
|
||||||
|
let selectedColor = UIColor.colorFF0089()
|
||||||
|
|
||||||
|
|
||||||
|
let backgroundImage = UIImage(color: .clear)
|
||||||
|
let backgroundColor = UIColor.clear
|
||||||
|
let shadowImage = UIImage()
|
||||||
|
let shadowColor = UIColor.clear
|
||||||
|
|
||||||
|
let par = NSMutableParagraphStyle()
|
||||||
|
par.alignment = .center;
|
||||||
|
|
||||||
|
let normalAttributes = [NSAttributedString.Key.foregroundColor : normalColor,
|
||||||
|
NSAttributedString.Key.paragraphStyle : par,
|
||||||
|
NSAttributedString.Key.font: font,
|
||||||
|
]
|
||||||
|
let selectedAttributes = [NSAttributedString.Key.foregroundColor:selectedColor,
|
||||||
|
NSAttributedString.Key.paragraphStyle : par,
|
||||||
|
NSAttributedString.Key.font: font,
|
||||||
|
]
|
||||||
|
|
||||||
|
let appearance = UITabBarAppearance()
|
||||||
|
|
||||||
|
let normal = appearance.stackedLayoutAppearance.normal
|
||||||
|
normal.titleTextAttributes = normalAttributes
|
||||||
|
|
||||||
|
let selected = appearance.stackedLayoutAppearance.selected
|
||||||
|
selected.titleTextAttributes = selectedAttributes
|
||||||
|
|
||||||
|
appearance.backgroundImage = backgroundImage;
|
||||||
|
appearance.backgroundColor = backgroundColor;
|
||||||
|
appearance.shadowImage = shadowImage
|
||||||
|
appearance.shadowColor = shadowColor
|
||||||
|
appearance.backgroundEffect = nil
|
||||||
|
// 官方文档写的是 重置背景和阴影为透明
|
||||||
|
// appearance.configureWithTransparentBackground()
|
||||||
|
tabBar.standardAppearance = appearance;
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
tabBar.scrollEdgeAppearance = appearance
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
ShortPlay/AppDelegate/AppDelegate.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
|
||||||
|
|
||||||
|
self.appConfig()
|
||||||
|
|
||||||
|
SPLoginManager.manager.requestVisitorLogin(completer: nil)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UISceneSession Lifecycle
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
// Called when a new scene session is being created.
|
||||||
|
// Use this method to select a configuration to create the new scene with.
|
||||||
|
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||||
|
// Called when the user discards a scene session.
|
||||||
|
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||||
|
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
53
ShortPlay/AppDelegate/SceneDelegate.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// SceneDelegate.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
|
||||||
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
|
guard let windowScene = (scene as? UIWindowScene) else { return }
|
||||||
|
|
||||||
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
window?.rootViewController = SPTabBarController()
|
||||||
|
window?.makeKeyAndVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
|
// Called as the scene is being released by the system.
|
||||||
|
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||||
|
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||||
|
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene has moved from an inactive state to an active state.
|
||||||
|
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene will move from an active state to an inactive state.
|
||||||
|
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the background to the foreground.
|
||||||
|
// Use this method to undo the changes made on entering the background.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the foreground to the background.
|
||||||
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
|
// to restore the scene back to its current state.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
35
ShortPlay/Base/Controller/SPNavigationController.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// SPNavigationController.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPNavigationController: UINavigationController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Do any additional setup after loading the view.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||||
|
if children.count > 0 {
|
||||||
|
viewController.hidesBottomBarWhenPushed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
super.pushViewController(viewController, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
|
||||||
|
for (index, value) in viewControllers.enumerated() {
|
||||||
|
if index != 0 {
|
||||||
|
value.hidesBottomBarWhenPushed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.setViewControllers(viewControllers, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
47
ShortPlay/Base/Controller/SPTabBarController.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// SPTabBarController.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPTabBarController: UITabBarController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
let nav1 = createNavigationController(viewController: SPHomePageController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
|
||||||
|
|
||||||
|
let nav2 = createNavigationController(viewController: SPForYouViewController(), title: "For You".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
|
||||||
|
|
||||||
|
self.viewControllers = [nav1, nav2]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//MARK:-------------- 状态栏样式 --------------
|
||||||
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return self.selectedViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
override var childForStatusBarHidden: UIViewController? {
|
||||||
|
return self.selectedViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPTabBarController {
|
||||||
|
|
||||||
|
func createNavigationController(viewController: UIViewController, title: String?, image: UIImage?, selectedImage: UIImage?) -> UINavigationController {
|
||||||
|
|
||||||
|
let nav = SPNavigationController(rootViewController: viewController)
|
||||||
|
nav.tabBarItem.selectedImage = selectedImage
|
||||||
|
nav.tabBarItem.image = image
|
||||||
|
nav.tabBarItem.title = title
|
||||||
|
|
||||||
|
return nav
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
155
ShortPlay/Base/Controller/SPViewController.swift
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
//
|
||||||
|
// SPViewController.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPViewController: UIViewController, JYPageChildContollerProtocol {
|
||||||
|
|
||||||
|
var statusBarStyle: UIStatusBarStyle? {
|
||||||
|
didSet {
|
||||||
|
self.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var statusBarHidden: Bool = false {
|
||||||
|
didSet {
|
||||||
|
// self.setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var isViewDidLoad = false
|
||||||
|
private(set) var isDidAppear = false
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.isViewDidLoad = true
|
||||||
|
self.edgesForExtendedLayout = []
|
||||||
|
|
||||||
|
if let navi = navigationController {
|
||||||
|
if navi.visibleViewController == self {
|
||||||
|
if navi.viewControllers.count > 1 {
|
||||||
|
configNavigationBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
isDidAppear = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
isDidAppear = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
isDidAppear = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//MARK:-------------- 状态栏样式 --------------
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
if let statusBarStyle = statusBarStyle {
|
||||||
|
return statusBarStyle
|
||||||
|
} else {
|
||||||
|
return .default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override var prefersStatusBarHidden: Bool {
|
||||||
|
return statusBarHidden
|
||||||
|
}
|
||||||
|
///是否支持自动旋屏
|
||||||
|
override var shouldAutorotate: Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
///屏幕方向
|
||||||
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
|
return .portrait
|
||||||
|
}
|
||||||
|
|
||||||
|
///子类实现
|
||||||
|
func fetchChildControllerScrollView() -> UIScrollView? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
|
||||||
|
func configNavigationBack(_ imageName: String = "left_arrow_icon_01") {
|
||||||
|
let image = UIImage(named: imageName)
|
||||||
|
|
||||||
|
let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleBack))
|
||||||
|
navigationItem.leftBarButtonItem = leftBarButtonItem
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@objc func handleBack() {
|
||||||
|
self.sp_toLastViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
|
||||||
|
|
||||||
|
func sp_toLastViewController(animated:Bool)
|
||||||
|
{
|
||||||
|
if self.navigationController != nil
|
||||||
|
{
|
||||||
|
if self.navigationController?.viewControllers.count == 1
|
||||||
|
{
|
||||||
|
self.dismiss(animated: animated, completion: nil)
|
||||||
|
} else {
|
||||||
|
self.navigationController?.popViewController(animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if self.presentingViewController != nil {
|
||||||
|
self.dismiss(animated: animated, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
///设置导航默认样式
|
||||||
|
func setNavigationNormalStyle(backgroundColor: UIColor = UINavigationBar.sp_normalBackgroundColor,
|
||||||
|
isTranslucent: Bool = false,
|
||||||
|
prefersLargeTitles: Bool = false)
|
||||||
|
{
|
||||||
|
self.setNavigationBackgroundColor(color: backgroundColor, isTranslucent: isTranslucent)
|
||||||
|
self.setNavigationTitleStyle()
|
||||||
|
self.navigationController?.navigationBar.prefersLargeTitles = prefersLargeTitles
|
||||||
|
}
|
||||||
|
|
||||||
|
///设置导航背景色
|
||||||
|
func setNavigationBackgroundColor(color: UIColor?, isTranslucent: Bool = false) {
|
||||||
|
guard let nav = navigationController else { return }
|
||||||
|
if nav.visibleViewController == self {
|
||||||
|
nav.navigationBar.sp_setBackgroundColor(backgroundColor: color)
|
||||||
|
nav.navigationBar.sp_setTranslucent(isTranslucent: isTranslucent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///设置当行标题样式
|
||||||
|
func setNavigationTitleStyle(color: UIColor? = nil, font: UIFont? = nil) {
|
||||||
|
guard let nav = navigationController else { return }
|
||||||
|
if nav.visibleViewController == self {
|
||||||
|
//标题样式
|
||||||
|
var titleTextAttributes = UINavigationBar.sp_normalTitleTextAttributes
|
||||||
|
if let color = color {
|
||||||
|
titleTextAttributes[NSAttributedString.Key.foregroundColor] = color
|
||||||
|
}
|
||||||
|
if let font = font {
|
||||||
|
titleTextAttributes[NSAttributedString.Key.font] = font
|
||||||
|
}
|
||||||
|
nav.navigationBar.sp_setTitleTextAttributes(titleTextAttributes: titleTextAttributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
ShortPlay/Base/Define/SPDefine.swift
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// SPDefine.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
//MARK:-------------- 屏幕尺寸相关 --------------
|
||||||
|
let kSPScreenWidth = UIScreen.main.bounds.size.width
|
||||||
|
let kSPScreenHeight = UIScreen.main.bounds.size.height
|
||||||
|
let kSPNavBarHeight : CGFloat = ((kSPStatusbarHeight != 59) ? kSPStatusbarHeight: 54) + 44 // 灵动岛
|
||||||
|
let kSPTabBarHeight : CGFloat = kSPTabbarSafeBottomMargin + 49
|
||||||
|
|
||||||
|
///状态栏高度
|
||||||
|
let kSPStatusbarHeight: CGFloat = {
|
||||||
|
var top: CGFloat = 20
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
top = UIApplication.shared.windows[0].safeAreaInsets.top
|
||||||
|
top = (top != 59) ? top : 54 //灵动岛
|
||||||
|
top = top == 0 ? 20 : top
|
||||||
|
}
|
||||||
|
return top
|
||||||
|
}()
|
||||||
|
///tab高度
|
||||||
|
let kSPTabbarSafeBottomMargin: CGFloat = {
|
||||||
|
var bottom: CGFloat = 0
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
bottom = UIApplication.shared.windows[0].safeAreaInsets.bottom
|
||||||
|
}
|
||||||
|
return bottom
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 从375自适应
|
||||||
|
public let kSPMainW:((CGFloat)-> CGFloat) = { (size : CGFloat) -> CGFloat in
|
||||||
|
return kSPWidthScale * size
|
||||||
|
}
|
||||||
|
///宽比
|
||||||
|
let kSPWidthScale = kSPScreenWidth / 375
|
||||||
|
|
||||||
|
//MARK:-------------- 系统版本号 --------------
|
||||||
|
///当前系统版本号
|
||||||
|
let kSP_osVersion: String = UIDevice.current.systemVersion
|
||||||
|
let kSPAPPBundleIdentifier: String = (Bundle.main.infoDictionary!["CFBundleIdentifier"] as? String) ?? "0"
|
||||||
|
|
||||||
|
///app版本号
|
||||||
|
public let kSPAPPVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0"
|
||||||
|
public let kSPAPPBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0"
|
||||||
|
|
||||||
|
public let kSPAPPBundleName: String = (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? ""
|
||||||
|
|
||||||
|
//MARK: ------- 打印信息 ----------
|
||||||
|
public func spLog(message:Any? , file: String = #file, function: String = #function, line: Int = #line) {
|
||||||
|
#if DEBUG
|
||||||
|
print("\n\(Date(timeIntervalSinceNow: 8 * 60 * 60)) \(file.components(separatedBy: "/").last ?? "") \(function) \(line): \(message ?? "")")
|
||||||
|
#endif
|
||||||
|
}
|
11
ShortPlay/Base/Define/SPUserDefaultsKey.swift
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// SPUserDefaultsKey.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
///登录的用户信息
|
||||||
|
let kSPLoginTokenDefaultsKey = "kSPLoginTokenDefaultsKey"
|
21
ShortPlay/Base/Extension/NSUserDefaults+JXAdd.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// NSUserDefaults+JXAdd.h
|
||||||
|
// 链之家
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2017/11/16.
|
||||||
|
// Copyright © 2017年 NetLoanHome. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface NSUserDefaults (JXAdd)
|
||||||
|
|
||||||
|
///NSObject 需要实现NSSecureCoding协议
|
||||||
|
+ (void)jx_setObject:(nullable id)obj forKey:(NSString *)key;
|
||||||
|
+ (nullable id)jx_objectForKey:(NSString *)key class:(Class)cls;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
45
ShortPlay/Base/Extension/NSUserDefaults+JXAdd.m
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// NSUserDefaults+JXAdd.m
|
||||||
|
// 链之家
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2017/11/16.
|
||||||
|
// Copyright © 2017年 NetLoanHome. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "NSUserDefaults+JXAdd.h"
|
||||||
|
|
||||||
|
@implementation NSUserDefaults (JXAdd)
|
||||||
|
|
||||||
|
+ (void)jx_setObject:(nullable id)obj forKey:(NSString *)key {
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
if (!obj) {
|
||||||
|
[defaults removeObjectForKey:key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ([obj respondsToSelector:@selector(encodeWithCoder:)] == NO) {
|
||||||
|
NSLog(@"Error save object to NSUserDefaults. Object must respond to encodeWithCoder: message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *error;
|
||||||
|
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:obj requiringSecureCoding:true error:&error];
|
||||||
|
// NSLog(@"%@", error);
|
||||||
|
[defaults setObject:encodedObject forKey:key];
|
||||||
|
[defaults synchronize];
|
||||||
|
}
|
||||||
|
+ (nullable id)jx_objectForKey:(NSString *)key class:(Class)cls {
|
||||||
|
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
NSData *encodedObject = [defaults objectForKey:key];
|
||||||
|
if (encodedObject && [encodedObject.class isSubclassOfClass:NSData.class]) {
|
||||||
|
NSError *error;
|
||||||
|
id obj = [NSKeyedUnarchiver unarchivedObjectOfClass:cls fromData:encodedObject error:&error];
|
||||||
|
// NSLog(@"%@", error);
|
||||||
|
return obj;
|
||||||
|
} else {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
28
ShortPlay/Base/Extension/String+SPAdd.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// String+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
extension String: SmartCodable {
|
||||||
|
|
||||||
|
func length() -> Int {
|
||||||
|
return self.ocString().length
|
||||||
|
}
|
||||||
|
|
||||||
|
func ocString() -> NSString {
|
||||||
|
return self as NSString
|
||||||
|
}
|
||||||
|
|
||||||
|
static func timeZone() -> String {
|
||||||
|
let timeZone = NSTimeZone.local as NSTimeZone
|
||||||
|
let timeZoneAbbreviation = timeZone.name.length() > 0 ? timeZone.name : "Unknown"
|
||||||
|
let timeZoneSecondsFromGMT = timeZone.secondsFromGMT / 3600
|
||||||
|
return String(format: "GMT+0%d:00", timeZoneSecondsFromGMT)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
47
ShortPlay/Base/Extension/UIColor+SPAdd.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// UIColor+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIColor {
|
||||||
|
|
||||||
|
static func color(hex: UInt32, alpha: CGFloat = 1) -> UIColor {
|
||||||
|
return UIColor(rgb: hex, alpha: alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// static func backgroundColor() -> UIColor {
|
||||||
|
// return colorF3F5F7()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
static func themeColor() -> UIColor {
|
||||||
|
return color000000()
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// static func placeholderColor() -> UIColor {
|
||||||
|
// return color868C92()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIColor {
|
||||||
|
static func colorFFFFFF(alpha: CGFloat = 1) -> UIColor {
|
||||||
|
return color(hex: 0xFFFFFF, alpha: alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func color000000(alpha: CGFloat = 1) -> UIColor {
|
||||||
|
return color(hex: 0x000000, alpha: alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func colorFF0089(alpha: CGFloat = 1) -> UIColor {
|
||||||
|
return color(hex: 0xFF0089, alpha: alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func color7F7F80(alpha: CGFloat = 1) -> UIColor {
|
||||||
|
return color(hex: 0x7F7F80, alpha: alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
73
ShortPlay/Base/Extension/UIDevice+SPAdd.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// UIDevice+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIDevice {
|
||||||
|
|
||||||
|
//http://theiphonewiki.com/wiki/Models
|
||||||
|
static func sp_machineModelName() -> String {
|
||||||
|
guard let machineModel = UIDevice.current.machineModel else { return "" }
|
||||||
|
let map = [
|
||||||
|
"iPhone1,1" : "iPhone",
|
||||||
|
"iPhone1,2" : "iPhone 3G",
|
||||||
|
"iPhone2,1" : "iPhone 3GS",
|
||||||
|
"iPhone3,1" : "iPhone 4",
|
||||||
|
"iPhone3,2" : "iPhone 4",
|
||||||
|
"iPhone3,3" : "iPhone 4",
|
||||||
|
"iPhone4,1" : "iPhone4,1",
|
||||||
|
"iPhone5,1" : "iPhone 5",
|
||||||
|
"iPhone5,2" : "iPhone 5",
|
||||||
|
"iPhone5,3" : "iPhone 5c",
|
||||||
|
"iPhone5,4" : "iPhone 5c",
|
||||||
|
"iPhone6,1" : "iPhone 5s",
|
||||||
|
"iPhone6,2" : "iPhone 5s",
|
||||||
|
"iPhone7,2" : "iPhone 6",
|
||||||
|
"iPhone7,1" : "iPhone 6 Plus",
|
||||||
|
"iPhone8,1" : "iPhone 6s",
|
||||||
|
"iPhone8,2" : "iPhone 6s Plus",
|
||||||
|
"iPhone8,4" : "iPhone SE (1st generation)",
|
||||||
|
"iPhone9,1" : "iPhone 7",
|
||||||
|
"iPhone9,3" : "iPhone 7",
|
||||||
|
"iPhone9,2" : "iPhone 7 Plus",
|
||||||
|
"iPhone9,4" : "iPhone 7 Plus",
|
||||||
|
"iPhone10,1" : "iPhone 8",
|
||||||
|
"iPhone10,4" : "iPhone 8",
|
||||||
|
"iPhone10,2" : "iPhone 8 Plus",
|
||||||
|
"iPhone10,5" : "iPhone 8 Plus",
|
||||||
|
"iPhone10,3" : "iPhone X",
|
||||||
|
"iPhone10,6" : "iPhone X",
|
||||||
|
"iPhone11,8" : "iPhone XR",
|
||||||
|
"iPhone11,2" : "iPhone11,2",
|
||||||
|
"iPhone11,6" : "iPhone XS Max",
|
||||||
|
"iPhone11,4" : "iPhone XS Max",
|
||||||
|
"iPhone12,1" : "iPhone 11",
|
||||||
|
"iPhone12,3" : "iPhone 11 Pro",
|
||||||
|
"iPhone12,5" : "iPhone 11 Pro Max",
|
||||||
|
"iPhone12,8" : "iPhone SE (2nd generation)",
|
||||||
|
"iPhone13,1" : "iPhone 12 mini",
|
||||||
|
"iPhone13,2" : "iPhone13,2",
|
||||||
|
"iPhone13,3" : "iPhone 12 Pro",
|
||||||
|
"iPhone13,4" : "iPhone 12 Pro Max",
|
||||||
|
"iPhone14,4" : "iPhone 13 mini",
|
||||||
|
"iPhone14,5" : "iPhone 13",
|
||||||
|
"iPhone14,2" : "iPhone 13 Pro",
|
||||||
|
"iPhone14,3" : "iPhone 13 Pro Max",
|
||||||
|
"iPhone14,6" : "iPhone SE (3rd generation)",
|
||||||
|
"iPhone14,7" : "iPhone 14",
|
||||||
|
"iPhone14,8" : "iPhone 14 Plus",
|
||||||
|
"iPhone15,2" : "iPhone 14 Pro",
|
||||||
|
"iPhone15,3" : "iPhone 14 Pro Max",
|
||||||
|
]
|
||||||
|
if let name = map[machineModel] {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineModel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
ShortPlay/Base/Extension/UIFont+SPAdd.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// UIFont+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIFont {
|
||||||
|
|
||||||
|
static func fontRegular(ofSize: CGFloat) -> UIFont {
|
||||||
|
return .systemFont(ofSize: ofSize, weight: .regular)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fontMedium(ofSize: CGFloat) -> UIFont {
|
||||||
|
return .systemFont(ofSize: ofSize, weight: .medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fontBold(ofSize: CGFloat) -> UIFont {
|
||||||
|
return .systemFont(ofSize: ofSize, weight: .bold)
|
||||||
|
}
|
||||||
|
}
|
24
ShortPlay/Base/Extension/UIImageView+SPAdd.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// UIImageView+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
extension UIImageView {
|
||||||
|
func sp_setImage(url: String?, placeholder: UIImage? = nil, completer: ((_ image: UIImage?, _ url: URL?) -> Void)? = nil) {
|
||||||
|
|
||||||
|
self.kf.setImage(with: URL(string: url ?? ""), placeholder: placeholder, options: nil) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let value):
|
||||||
|
completer?(value.image, value.source.url)
|
||||||
|
default :
|
||||||
|
completer?(nil, nil)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
ShortPlay/Base/Extension/UINavigationBar+SPAdd.swift
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// UINavigationBar+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
extension UINavigationBar {
|
||||||
|
|
||||||
|
static let sp_normalTitleFont = UIFont.fontBold(ofSize: 15)
|
||||||
|
static var sp_normalTitleColor: UIColor {
|
||||||
|
get {
|
||||||
|
return .colorFFFFFF()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
默认标题样式
|
||||||
|
*/
|
||||||
|
static var sp_normalTitleTextAttributes: [NSAttributedString.Key : Any] {
|
||||||
|
get {
|
||||||
|
return [
|
||||||
|
NSAttributedString.Key.font : sp_normalTitleFont,
|
||||||
|
NSAttributedString.Key.foregroundColor : sp_normalTitleColor
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
默认背景色
|
||||||
|
*/
|
||||||
|
static var sp_normalBackgroundColor: UIColor {
|
||||||
|
get {
|
||||||
|
return .themeColor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
static let navBarAppearance: UINavigationBarAppearance = {
|
||||||
|
let navBarAppearance = UINavigationBarAppearance()
|
||||||
|
navBarAppearance.configureWithOpaqueBackground()
|
||||||
|
// 背景色
|
||||||
|
navBarAppearance.backgroundColor = sp_normalBackgroundColor
|
||||||
|
// 去掉半透明效果
|
||||||
|
navBarAppearance.backgroundEffect = nil
|
||||||
|
// 去除导航栏阴影(如果不设置clear,导航栏底下会有一条阴影线)
|
||||||
|
navBarAppearance.shadowColor = UIColor.clear
|
||||||
|
// 字体颜色
|
||||||
|
navBarAppearance.titleTextAttributes = sp_normalTitleTextAttributes
|
||||||
|
|
||||||
|
return navBarAppearance
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
func sp_setTranslucent(isTranslucent: Bool) {
|
||||||
|
self.isTranslucent = isTranslucent
|
||||||
|
}
|
||||||
|
|
||||||
|
func sp_setBackgroundColor(backgroundColor: UIColor?) {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
UINavigationBar.navBarAppearance.backgroundColor = backgroundColor
|
||||||
|
self.standardAppearance = UINavigationBar.navBarAppearance
|
||||||
|
self.scrollEdgeAppearance = UINavigationBar.navBarAppearance
|
||||||
|
}
|
||||||
|
|
||||||
|
if let backgroundColor = backgroundColor {
|
||||||
|
self.setBackgroundImage(UIImage(color: backgroundColor), for: .default)
|
||||||
|
// self.barTintColor = backgroundColor
|
||||||
|
} else {
|
||||||
|
self.setBackgroundImage(UIImage(), for: .default)
|
||||||
|
// self.barTintColor = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sp_setTitleTextAttributes(titleTextAttributes: [NSAttributedString.Key : Any]?) {
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
if let titleTextAttributes = titleTextAttributes {
|
||||||
|
UINavigationBar.navBarAppearance.titleTextAttributes = titleTextAttributes
|
||||||
|
}
|
||||||
|
self.scrollEdgeAppearance = UINavigationBar.navBarAppearance
|
||||||
|
self.standardAppearance = UINavigationBar.navBarAppearance
|
||||||
|
} else {
|
||||||
|
|
||||||
|
self.titleTextAttributes = titleTextAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
30
ShortPlay/Base/Extension/UINavigationController+SPAdd.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// UINavigationController+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UINavigationController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
跳转到新页面,并关闭上个页面
|
||||||
|
*/
|
||||||
|
func pushViewControllerAndDismissLastViewController(viewController: UIViewController, animated: Bool) {
|
||||||
|
var viewControllers = self.viewControllers
|
||||||
|
viewControllers.removeLast()
|
||||||
|
viewControllers.append(viewController)
|
||||||
|
self.setViewControllers(viewControllers, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK:-------------- 状态栏样式 --------------
|
||||||
|
open override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return self.topViewController
|
||||||
|
}
|
||||||
|
open override var childForStatusBarHidden: UIViewController? {
|
||||||
|
return self.topViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
ShortPlay/Base/Extension/UIView+SPAdd.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// UIView+SPAdd.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SnapKit
|
||||||
|
|
||||||
|
extension UIView {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
22
ShortPlay/Base/Model/SPListModel.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// SPListModel.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class SPListModel<T: SmartCodable>: SPModel, SmartCodable {
|
||||||
|
var list: [T]?
|
||||||
|
var pagination: SPListPaginationModel?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SPListPaginationModel: SPModel, SmartCodable {
|
||||||
|
var current_page: Int?
|
||||||
|
var page_size: Int?
|
||||||
|
var page_total: Int?
|
||||||
|
var total_size: Int?
|
||||||
|
}
|
15
ShortPlay/Base/Model/SPModel.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// SPModel.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class SPModel: NSObject {
|
||||||
|
required override init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
27
ShortPlay/Base/Networking/API/SPHomeAPI.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// SPHomeAPI.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPHomeAPI: NSObject {
|
||||||
|
|
||||||
|
///推荐短剧
|
||||||
|
static func requestRecommandsTV(page: Int, completer: ((_ listModel: SPListModel<SPShortModel>?) -> Void)?) {
|
||||||
|
|
||||||
|
var param = SPNetworkParameters(path: "/getRecommands")
|
||||||
|
param.method = .get
|
||||||
|
param.parameters = [
|
||||||
|
"page_size" : 20,
|
||||||
|
"current_page" : page
|
||||||
|
]
|
||||||
|
|
||||||
|
SPNetwork.request(parameters: param) { (response: SPNetworkResponse<SPListModel<SPShortModel>>) in
|
||||||
|
completer?(response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
117
ShortPlay/Base/Networking/Base/SPApi.swift
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
//
|
||||||
|
// SPApi.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Moya
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
struct SPNetworkData<T: SmartCodable> {
|
||||||
|
var parameters: SPNetworkParameters?
|
||||||
|
var completion: ((_ response: SPNetworkResponse<T>) -> Void)?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SPNetworkParameters {
|
||||||
|
var baseURL: URL?
|
||||||
|
var parameters: [String : Any]?
|
||||||
|
var method: Moya.Method = .post
|
||||||
|
var path: String
|
||||||
|
var isLoding: Bool = false
|
||||||
|
var isToast: Bool = true
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SPNetworkResponse<T: SmartCodable>: SmartCodable {
|
||||||
|
var code: Int?
|
||||||
|
var data: T?
|
||||||
|
var msg: String?
|
||||||
|
|
||||||
|
///原始数据
|
||||||
|
@IgnoredKey
|
||||||
|
var rawData: Any?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SPApi {
|
||||||
|
case request(parameters: SPNetworkParameters)
|
||||||
|
case uploadFile(parameters: SPNetworkParameters)
|
||||||
|
case downloadFile(parameters: SPNetworkParameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPApi: TargetType {
|
||||||
|
|
||||||
|
|
||||||
|
var baseURL: URL {
|
||||||
|
return URL(string: SPBaseURL)!
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var path: String {
|
||||||
|
switch self {
|
||||||
|
case .request(let parameters):
|
||||||
|
return parameters.path
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: Moya.Method {
|
||||||
|
switch self {
|
||||||
|
case .request(let parameters):
|
||||||
|
return parameters.method
|
||||||
|
default:
|
||||||
|
return .post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task: Moya.Task {
|
||||||
|
switch self {
|
||||||
|
case .request(let parameters):
|
||||||
|
let p = parameters.parameters ?? [:]
|
||||||
|
return .requestParameters(parameters: p, encoding: getEncoding())
|
||||||
|
default:
|
||||||
|
return .requestParameters(parameters: [:], encoding: getEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers: [String : String]? {
|
||||||
|
let userToken = SPLoginManager.manager.token?.token ?? ""
|
||||||
|
|
||||||
|
var dic: [String : String] = [
|
||||||
|
"system-version" : kSP_osVersion,
|
||||||
|
"lang-key" : SPLocalizedManager.shared.currentLocalizedKey,//当前语言
|
||||||
|
"time-zone" : String.timeZone(), //时区
|
||||||
|
"app-version" : kSPAPPVersion,
|
||||||
|
"device-id" : JXUUID.systemUUID(), //设备id
|
||||||
|
"brand" : "apple", //品牌
|
||||||
|
"app-name" : "",
|
||||||
|
"system-type" : "ios",
|
||||||
|
"idfa" : JXUUID.idfa(),
|
||||||
|
"model" : UIDevice.sp_machineModelName(),
|
||||||
|
]
|
||||||
|
//登录信息
|
||||||
|
dic["authorization"] = userToken
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return dic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TargetType {
|
||||||
|
var sampleData: Data { return "".data(using: String.Encoding.utf8)! }
|
||||||
|
|
||||||
|
func getEncoding() -> ParameterEncoding {
|
||||||
|
switch self.method {
|
||||||
|
case .get, .delete:
|
||||||
|
return URLEncoding.default
|
||||||
|
default:
|
||||||
|
return JSONEncoding.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
179
ShortPlay/Base/Networking/Base/SPNetwork.swift
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
//
|
||||||
|
// SPNetwork.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Moya
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
///获取数据成功
|
||||||
|
let SPNetworkCodeSucceed = 200
|
||||||
|
|
||||||
|
class SPNetwork: NSObject {
|
||||||
|
|
||||||
|
///等待请求的数据
|
||||||
|
private static var awaitDataArr: [Any] = []
|
||||||
|
|
||||||
|
private static var group = DispatchGroup()
|
||||||
|
private static var queue: DispatchQueue = DispatchQueue(label: "bcRequestQueue", attributes: .concurrent)
|
||||||
|
static let semaphore = DispatchSemaphore(value: 1) // 只允许 1 个任务执行
|
||||||
|
|
||||||
|
static let provider = MoyaProvider<SPApi>(requestClosure: CustomApiTimeoutClosure)
|
||||||
|
|
||||||
|
static func request<T>(parameters: SPNetworkParameters, completion: ((_ response: SPNetworkResponse<T>) -> Void)?) {
|
||||||
|
|
||||||
|
if SPLoginManager.manager.token == nil {
|
||||||
|
SPLoginManager.manager.requestVisitorLogin(completer: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if SPLoginManager.manager.isRefreshingToken && parameters.path != "/customer/register" {
|
||||||
|
var loding = true
|
||||||
|
while loding {
|
||||||
|
if !SPLoginManager.manager.isRefreshingToken {
|
||||||
|
loding = false
|
||||||
|
}
|
||||||
|
RunLoop.current.run(mode: .default, before: Date.distantFuture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_request(parameters: parameters, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func _request<T>(parameters: SPNetworkParameters, completion: ((_ response: SPNetworkResponse<T>) -> Void)?) -> Cancellable {
|
||||||
|
|
||||||
|
if parameters.isLoding {
|
||||||
|
// ETHUD.show()
|
||||||
|
}
|
||||||
|
return provider.requestCustomJson(.request(parameters: parameters)) { (result) in
|
||||||
|
|
||||||
|
if parameters.isLoding {
|
||||||
|
// ETHUD.dismiss()
|
||||||
|
}
|
||||||
|
guard let completion = completion else {return}
|
||||||
|
|
||||||
|
|
||||||
|
_resultDispose(parameters: parameters, result: result, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func _resultDispose<T>(parameters: SPNetworkParameters, result: Result<Moya.Response, MoyaError>, completion: ((_ response: SPNetworkResponse<T>) -> Void)?) {
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
let code = response.statusCode
|
||||||
|
if code == 401 || code == 402 || code == 403 {
|
||||||
|
// var data = SPNetworkData<T>()
|
||||||
|
// data.parameters = parameters
|
||||||
|
// data.completion = completion
|
||||||
|
//
|
||||||
|
// awaitDataArr.append(data)
|
||||||
|
// awaitDataArr.first as? SPNetworkData<T>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if !SPLoginManager.manager.isRefreshingToken {
|
||||||
|
SPLoginManager.manager.requestVisitorLogin {
|
||||||
|
if let _ = SPLoginManager.manager.token {
|
||||||
|
self.request(parameters: parameters, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if parameters.path != "/customer/register" {
|
||||||
|
// while SPLoginManager.manager.isRefreshingToken {
|
||||||
|
// RunLoop.current.run(mode: .default, before: Date.distantFuture)
|
||||||
|
// }
|
||||||
|
// if let _ = SPLoginManager.manager.token {
|
||||||
|
// self.request(parameters: parameters, completion: completion)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let tempData: [String : Any] = try response.mapJSON() as! [String : Any]
|
||||||
|
spLog(message: parameters.parameters)
|
||||||
|
spLog(message: parameters.path)
|
||||||
|
spLog(message: tempData as NSDictionary)
|
||||||
|
var response = SPNetworkResponse<T>.deserialize(from: tempData)
|
||||||
|
|
||||||
|
if response != nil {
|
||||||
|
|
||||||
|
if response?.code == SPNetworkCodeSucceed {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if parameters.isToast {
|
||||||
|
SPToast.show(text: response?.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response?.rawData = tempData
|
||||||
|
completion?(response!)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
response = SPNetworkResponse<T>()
|
||||||
|
response?.code = -1
|
||||||
|
if parameters.isToast {
|
||||||
|
SPToast.show(text: "Error".localized)
|
||||||
|
// ETHUD.showToast(text: "系统错误".localized)
|
||||||
|
}
|
||||||
|
completion?(response!)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
var response = SPNetworkResponse<T>()
|
||||||
|
response.code = -1
|
||||||
|
if parameters.isToast {
|
||||||
|
SPToast.show(text: "Error".localized)
|
||||||
|
// ETHUD.showToast(text: "系统错误".localized)
|
||||||
|
}
|
||||||
|
completion?(response)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
spLog(message: error)
|
||||||
|
var response = SPNetworkResponse<T>()
|
||||||
|
response.code = -1
|
||||||
|
if parameters.isToast {
|
||||||
|
SPToast.show(text: "Error".localized)
|
||||||
|
// ETHUD.showToast(text: "网络异常".localized)
|
||||||
|
}
|
||||||
|
completion?(response)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MoyaProvider {
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func requestCustomJson(_ target: Target, callbackQueue: DispatchQueue? = nil, completion: Completion?) -> Cancellable {
|
||||||
|
return request(target, callbackQueue: callbackQueue) { (result) in
|
||||||
|
guard let completion = completion else {return}
|
||||||
|
completion(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let CustomApiTimeoutClosure = {(endpoint: Endpoint, closure: MoyaProvider<SPApi>.RequestResultClosure) -> Void in
|
||||||
|
|
||||||
|
if var urlRequest = try? endpoint.urlRequest() {
|
||||||
|
///总是获取新数据
|
||||||
|
urlRequest.cachePolicy = .reloadIgnoringCacheData
|
||||||
|
urlRequest.timeoutInterval = 30
|
||||||
|
closure(.success(urlRequest))
|
||||||
|
} else {
|
||||||
|
closure(.failure(MoyaError.requestMapping(endpoint.url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG ///
|
||||||
|
//print(try? endpoint.urlRequest() )
|
||||||
|
#endif
|
||||||
|
}
|
18
ShortPlay/Base/Networking/Base/SPURLPath.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// SPURLPath.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
let SPBaseURL = "https://test1-api.guyantv.com"
|
||||||
|
let SPWebBaseURL = "https://test1-api.guyantv.com"
|
||||||
|
#else
|
||||||
|
let SPBaseURL = "https://test1-api.guyantv.com"
|
||||||
|
let SPWebBaseURL = "https://test1-api.guyantv.com"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
22
ShortPlay/Base/View/SPCollectionView.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// SPCollectionView.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPCollectionView: UICollectionView {
|
||||||
|
|
||||||
|
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
|
||||||
|
super.init(frame: frame, collectionViewLayout: layout)
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
self.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
ShortPlay/Base/View/SPCollectionViewCell.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// SPCollectionViewCell.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPCollectionViewCell: UICollectionViewCell {
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
self.layer.rasterizationScale = UIScreen.main.scale
|
||||||
|
self.layer.shouldRasterize = true
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UICollectionViewCell {
|
||||||
|
// MARK: - 注册
|
||||||
|
public static func registerCell(collectionView: UICollectionView, _ reuseIdentifier: String = "reuseIdentifier") {
|
||||||
|
let reuseIdentifier = reuseIdentifier == "reuseIdentifier" ? NSStringFromClass(self) : reuseIdentifier
|
||||||
|
collectionView.register(self, forCellWithReuseIdentifier: reuseIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static func registerNibCell(collectionView: UICollectionView, _ reuseIdentifier: String = "reuseIdentifier") {
|
||||||
|
let reuseIdentifier = reuseIdentifier == "reuseIdentifier" ? NSStringFromClass(self) : reuseIdentifier
|
||||||
|
collectionView.register(UINib(nibName: "\(self)", bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - 复用取值
|
||||||
|
public static func dequeueReusableCell(collectionView: UICollectionView, indexPath: IndexPath , _ reuseIdentifier: String = "reuseIdentifier") -> Self{
|
||||||
|
|
||||||
|
let reuseIdentifier = reuseIdentifier == "reuseIdentifier" ? NSStringFromClass(self) : reuseIdentifier
|
||||||
|
return collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! Self
|
||||||
|
}
|
||||||
|
}
|
54
ShortPlay/Base/View/SPImageView.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// SPImageView.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPImageView: UIImageView {
|
||||||
|
|
||||||
|
var placeholderColor = UIColor.gray
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(image: UIImage?) {
|
||||||
|
super.init(image: image)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(image: UIImage?, highlightedImage: UIImage?) {
|
||||||
|
super.init(image: image, highlightedImage: highlightedImage)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _init() {
|
||||||
|
self.contentMode = .scaleAspectFill
|
||||||
|
self.layer.masksToBounds = true
|
||||||
|
if image == nil {
|
||||||
|
self.backgroundColor = self.placeholderColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var image: UIImage? {
|
||||||
|
didSet {
|
||||||
|
if self.backgroundColor == nil && image == nil {
|
||||||
|
self.backgroundColor = self.placeholderColor
|
||||||
|
} else if image != nil {
|
||||||
|
if self.backgroundColor == self.placeholderColor {
|
||||||
|
self.backgroundColor = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
58
ShortPlay/Class/Home/Controller/SPHomePageController.swift
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// SPHomePageController.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPHomePageController: SPViewController {
|
||||||
|
|
||||||
|
private lazy var pageView: JYPageController = {
|
||||||
|
let pageView = JYPageController()
|
||||||
|
pageView.delegate = self
|
||||||
|
pageView.dataSource = self
|
||||||
|
return pageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: -------------- JYPageControllerDelegate & JYPageControllerDataSource --------------
|
||||||
|
extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource {
|
||||||
|
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {
|
||||||
|
return .init(x: 0, y: kSPStatusbarHeight + 10, width: kSPScreenWidth, height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageController(_ pageController: JYPageController, frameForContainerView container: UIScrollView) -> CGRect {
|
||||||
|
return .init(x: 0, y: 0, width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageController(_ pageController: JYPageController, titleAt index: Int) -> String {
|
||||||
|
return "123"
|
||||||
|
}
|
||||||
|
|
||||||
|
func childController(atIndex index: Int) -> any JYPageChildContollerProtocol {
|
||||||
|
return SPViewController()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func numberOfChildControllers() -> Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
21
ShortPlay/Class/Home/Controller/SPHomeViewController.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// SPHomeViewController.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPHomeViewController: SPViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
16
ShortPlay/Class/Home/Model/SPHomeCategoryModel.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// SPHomeCategoryModel.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPHomeCategoryModel: SPModel {
|
||||||
|
|
||||||
|
|
||||||
|
var category_name: String?
|
||||||
|
var category_id: String?
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
28
ShortPlay/Class/Player/Model/SPPlayerProtocol.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// SPPlayerProtocol.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol SPPlayerProtocol: NSObjectProtocol {
|
||||||
|
|
||||||
|
///播放完成
|
||||||
|
var playerFinishHadle: (() -> Void)? { get set }
|
||||||
|
|
||||||
|
var model: Any? { get set }
|
||||||
|
|
||||||
|
var isCurrent: Bool { get set }
|
||||||
|
|
||||||
|
///播放准备
|
||||||
|
func prepare()
|
||||||
|
|
||||||
|
///开始播放
|
||||||
|
func start()
|
||||||
|
|
||||||
|
///暂停播放
|
||||||
|
func pause()
|
||||||
|
|
||||||
|
}
|
37
ShortPlay/Class/Player/Model/SPShortModel.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// SPShortModel.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class SPShortModel: SPModel, SmartCodable {
|
||||||
|
|
||||||
|
var all_coins: String?
|
||||||
|
var buy_type: String?
|
||||||
|
var collect_total: String?
|
||||||
|
var sp_description: String?
|
||||||
|
var episode_total: Int?
|
||||||
|
var horizontally_img: String?
|
||||||
|
var id: String?
|
||||||
|
var image_url: String?
|
||||||
|
var is_collect: Bool?
|
||||||
|
var name: String?
|
||||||
|
var process: String?
|
||||||
|
var search_click_total: String?
|
||||||
|
var short_id: String?
|
||||||
|
var short_play_id: String?
|
||||||
|
var tag_type: String?
|
||||||
|
var video_info: SPVideoInfoModel?
|
||||||
|
var watch_total: Int?
|
||||||
|
|
||||||
|
|
||||||
|
static func mappingForKey() -> [SmartKeyTransformer]? {
|
||||||
|
return [
|
||||||
|
CodingKeys.sp_description <--- ["description"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
26
ShortPlay/Class/Player/Model/SPVideoInfoModel.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// SPVideoInfoModel.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class SPVideoInfoModel: SPModel, SmartCodable {
|
||||||
|
|
||||||
|
var coins: Int?
|
||||||
|
var vip_coins: Int?
|
||||||
|
var episode: Int?
|
||||||
|
var id: String?
|
||||||
|
var image_url: String?
|
||||||
|
var is_vip: Int?
|
||||||
|
var promise_view_ad: Int?
|
||||||
|
// var revolution: []
|
||||||
|
var short_id: String?
|
||||||
|
var short_play_id: String?
|
||||||
|
var short_play_video_id: String?
|
||||||
|
var video_url: String?
|
||||||
|
|
||||||
|
}
|
97
ShortPlay/Class/Player/View/SPPlayerControlView.swift
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// SPPlayerControlView.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPPlayerControlView: UIView {
|
||||||
|
|
||||||
|
///滑动进度条
|
||||||
|
var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)?
|
||||||
|
|
||||||
|
///0-1
|
||||||
|
var progress: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
progressView.progress = progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) lazy var progressView: SPPlayerProgressView = {
|
||||||
|
let view = SPPlayerProgressView()
|
||||||
|
view.panStart = { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.panProgressStart()
|
||||||
|
}
|
||||||
|
view.panChange = { [weak self] progress in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.panProgressChange(progress: progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.panFinish = { [weak self] progress in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.panProgressFinish(progress: progress)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
sp_setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPPlayerControlView {
|
||||||
|
|
||||||
|
private func sp_setupUI() {
|
||||||
|
addSubview(progressView)
|
||||||
|
|
||||||
|
progressView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(10)
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.bottom.equalToSuperview().offset(-20)
|
||||||
|
make.height.equalTo(30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPPlayerControlView {
|
||||||
|
|
||||||
|
///滑动进度开始
|
||||||
|
private func panProgressStart() {
|
||||||
|
// self.isHidden = true
|
||||||
|
|
||||||
|
// screenProgressView.duration = self.player?.duration ?? 0
|
||||||
|
// screenProgressView.frame = self.window?.bounds ?? .zero
|
||||||
|
//
|
||||||
|
// self.window?.addSubview(screenProgressView)
|
||||||
|
//
|
||||||
|
// var point = self.progressView.convert(CGPoint(x: 0, y: 0), to: self.window)
|
||||||
|
// point.y = point.y + self.progressView.size.height - self.progressView.lineWidth / 2
|
||||||
|
//
|
||||||
|
// screenProgressView.point = point
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
///滑动进度中
|
||||||
|
private func panProgressChange(progress: CGFloat) {
|
||||||
|
// screenProgressView.progress = progress
|
||||||
|
}
|
||||||
|
|
||||||
|
///滑动进度结束
|
||||||
|
private func panProgressFinish(progress: CGFloat) {
|
||||||
|
// self.isHidden = false
|
||||||
|
// screenProgressView.removeFromSuperview()
|
||||||
|
self.panProgressFinishBlock?(progress)
|
||||||
|
}
|
||||||
|
}
|
121
ShortPlay/Class/Player/View/SPPlayerListCell.swift
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
//
|
||||||
|
// SPPlayerListCell.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var player: SPPlayer = {
|
||||||
|
let player = SPPlayer()
|
||||||
|
player.playerView = playerView
|
||||||
|
player.delegate = self
|
||||||
|
return player
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var playerView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var coverImageView: SPImageView = {
|
||||||
|
let imageView = SPImageView()
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var controlView: SPPlayerControlView = {
|
||||||
|
let view = SPPlayerControlView()
|
||||||
|
view.panProgressFinishBlock = { [weak self] progress in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let duration = CGFloat(self.player.duration)
|
||||||
|
let toTime = progress * duration
|
||||||
|
self.player.seekToTime(toTime: Int(toTime))
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
sp_setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: SPPlayerProtocol
|
||||||
|
var model: Any? {
|
||||||
|
didSet {
|
||||||
|
guard let model = model as? SPShortModel else { return }
|
||||||
|
player.setPlayUrl(url: model.video_info?.video_url ?? "")
|
||||||
|
coverImageView.sp_setImage(url: model.image_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCurrent: Bool = false
|
||||||
|
|
||||||
|
///播放完成
|
||||||
|
var playerFinishHadle: (() -> Void)?
|
||||||
|
|
||||||
|
func prepare() {
|
||||||
|
// player.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
player.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pause() {
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPPlayerListCell {
|
||||||
|
|
||||||
|
private func sp_setupUI() {
|
||||||
|
contentView.addSubview(coverImageView)
|
||||||
|
contentView.addSubview(playerView)
|
||||||
|
contentView.addSubview(controlView)
|
||||||
|
|
||||||
|
coverImageView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
playerView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
controlView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: -------------- SPPlayerDelegate --------------
|
||||||
|
extension SPPlayerListCell: SPPlayerDelegate {
|
||||||
|
|
||||||
|
func sp_playCompletion(_ player: SPPlayer) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func sp_playLoadingEnd(_ player: SPPlayer) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int) {
|
||||||
|
controlView.progress = CGFloat(currentTime) / CGFloat(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sp_firstRenderedStart(_ player: SPPlayer) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
125
ShortPlay/Class/Player/View/SPPlayerProgressView.swift
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
//
|
||||||
|
// SPPlayerProgressView.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPPlayerProgressView: UIView {
|
||||||
|
|
||||||
|
///滑动开始
|
||||||
|
var panStart: (() -> Void)?
|
||||||
|
|
||||||
|
///滑动中
|
||||||
|
var panChange: ((_ progress: CGFloat) -> Void)?
|
||||||
|
|
||||||
|
///滑动完成回调
|
||||||
|
var panFinish: ((_ progress: CGFloat) -> Void)?
|
||||||
|
|
||||||
|
var progress: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
if !isPaning {
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///用来记录滑动时的当前进度
|
||||||
|
private var tempProgress: CGFloat = 0
|
||||||
|
|
||||||
|
///滑动进度
|
||||||
|
private var panProgress: CGFloat = 0
|
||||||
|
|
||||||
|
var progressColor: UIColor = .red
|
||||||
|
var currentProgress: UIColor = .white
|
||||||
|
|
||||||
|
var lineWidth: CGFloat = 2
|
||||||
|
|
||||||
|
///是否在滑动中
|
||||||
|
private var isPaning: Bool = false
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
|
||||||
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
|
||||||
|
self.addGestureRecognizer(pan)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:)))
|
||||||
|
self.addGestureRecognizer(tap)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func draw(_ rect: CGRect) {
|
||||||
|
super.draw(rect)
|
||||||
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||||
|
let width = rect.width
|
||||||
|
let height = rect.height
|
||||||
|
|
||||||
|
var progress = self.progress
|
||||||
|
if self.isPaning {
|
||||||
|
progress = self.panProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
///绘制进度
|
||||||
|
let progressPath = UIBezierPath(roundedRect: CGRect(x: 0, y: height - lineWidth, width: width, height: lineWidth), cornerRadius: lineWidth / 2)
|
||||||
|
context.addPath(progressPath.cgPath)
|
||||||
|
context.setFillColor(progressColor.cgColor)
|
||||||
|
context.fillPath()
|
||||||
|
|
||||||
|
///绘制当前进度
|
||||||
|
let currentPath = UIBezierPath(roundedRect: CGRect(x: 0, y: height - lineWidth, width: width * progress, height: lineWidth), cornerRadius: lineWidth / 2)
|
||||||
|
context.addPath(currentPath.cgPath)
|
||||||
|
context.setFillColor(currentProgress.cgColor)
|
||||||
|
context.fillPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPPlayerProgressView {
|
||||||
|
|
||||||
|
@objc func handlePanGesture(sender: UIPanGestureRecognizer) {
|
||||||
|
|
||||||
|
switch sender.state {
|
||||||
|
case .began:
|
||||||
|
self.isPaning = true
|
||||||
|
self.tempProgress = self.progress
|
||||||
|
sender.setTranslation(CGPoint(x: 0, y: 0), in: self)
|
||||||
|
self.panStart?()
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
let point = sender.translation(in: self)
|
||||||
|
let offsetX = point.x / self.width
|
||||||
|
self.panProgress = self.tempProgress + offsetX
|
||||||
|
if self.panProgress < 0 {
|
||||||
|
self.panProgress = 0
|
||||||
|
}
|
||||||
|
self.panChange?(self.panProgress)
|
||||||
|
setNeedsDisplay()
|
||||||
|
|
||||||
|
default:
|
||||||
|
self.isPaning = false
|
||||||
|
self.panFinish?(self.panProgress)
|
||||||
|
|
||||||
|
self.panProgress = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleTapGesture(sender: UITapGestureRecognizer) {
|
||||||
|
let point = sender.location(in: self)
|
||||||
|
let offsetX = point.x / self.width
|
||||||
|
self.panFinish?(offsetX)
|
||||||
|
}
|
||||||
|
}
|
29
ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// SPPlayerListViewModel.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPPlayerListViewModel: NSObject {
|
||||||
|
|
||||||
|
var isPlaying = false
|
||||||
|
|
||||||
|
private var _currentPlayer: SPPlayerProtocol?
|
||||||
|
var currentPlayer: SPPlayerProtocol? {
|
||||||
|
set {
|
||||||
|
_currentPlayer?.isCurrent = false
|
||||||
|
_currentPlayer?.pause()
|
||||||
|
|
||||||
|
_currentPlayer = newValue
|
||||||
|
_currentPlayer?.isCurrent = true
|
||||||
|
}
|
||||||
|
get {
|
||||||
|
return _currentPlayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
93
ShortPlay/Libs/APPTool/SPAPPTool.swift
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// SPAPPTool.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPAPPTool: NSObject {
|
||||||
|
|
||||||
|
static func getAppDelegate() -> AppDelegate? {
|
||||||
|
return UIApplication.shared.delegate as? AppDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
///获得启动图
|
||||||
|
static func getLanuchViewController() -> UIViewController {
|
||||||
|
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
|
||||||
|
let vc = storyboard.instantiateViewController(withIdentifier: "LaunchScreen")
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
|
||||||
|
///获取根视图
|
||||||
|
static func rootViewController() -> UIViewController? {
|
||||||
|
return getKeyWindow()?.rootViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
///获取最上方视图
|
||||||
|
static func topViewController() -> UIViewController? {
|
||||||
|
var resultVC: UIViewController? = self.rootViewController()
|
||||||
|
if let rootNav = resultVC as? UINavigationController {
|
||||||
|
resultVC = rootNav.topViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
resultVC = self._topViewController(resultVC)
|
||||||
|
while resultVC?.presentedViewController != nil {
|
||||||
|
resultVC = self._topViewController(resultVC?.presentedViewController)
|
||||||
|
}
|
||||||
|
return resultVC
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func _topViewController(_ vc: UIViewController?) -> UIViewController? {
|
||||||
|
if vc is UINavigationController {
|
||||||
|
return _topViewController((vc as? UINavigationController)?.topViewController)
|
||||||
|
} else if vc is UITabBarController {
|
||||||
|
return _topViewController((vc as? UITabBarController)?.selectedViewController)
|
||||||
|
} else {
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getKeyWindow() -> UIWindow? {
|
||||||
|
var window: UIWindow?
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
window = UIApplication.shared.connectedScenes
|
||||||
|
.filter({$0.activationState == .foregroundActive})
|
||||||
|
.map({$0 as? UIWindowScene})
|
||||||
|
.compactMap({$0})
|
||||||
|
.first?.windows
|
||||||
|
.filter({$0.isKeyWindow}).first
|
||||||
|
}
|
||||||
|
if window == nil {
|
||||||
|
window = UIApplication.shared.windows.first { $0.isKeyWindow }
|
||||||
|
}
|
||||||
|
if window == nil {
|
||||||
|
window = UIApplication.shared.keyWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
return window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPAPPTool {
|
||||||
|
///剪贴板复制
|
||||||
|
static func copy(text: String?) {
|
||||||
|
if let text = text {
|
||||||
|
let copy = UIPasteboard.general
|
||||||
|
copy.string = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
震动反馈
|
||||||
|
*/
|
||||||
|
static func impactFeedbackOccurred() {
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
UIImpactFeedbackGenerator(style: .heavy).impactOccurred(intensity: 1)
|
||||||
|
} else {
|
||||||
|
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
ShortPlay/Libs/HUD/SPToast.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// SPToast.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPToast: NSObject {
|
||||||
|
|
||||||
|
static func show(text: String?) {
|
||||||
|
guard let text = text else { return }
|
||||||
|
SPAPPTool.getKeyWindow()?.makeToast(text, duration: 2, position: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
57
ShortPlay/Libs/Login/SPLoginManager.swift
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// SPLoginManager.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPLoginManager: NSObject {
|
||||||
|
|
||||||
|
static let manager = SPLoginManager()
|
||||||
|
|
||||||
|
private(set) var token: SPTokenModel?
|
||||||
|
|
||||||
|
///是否正在刷新token
|
||||||
|
private(set) var isRefreshingToken = false
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
token = UserDefaults.jx_object(forKey: kSPLoginTokenDefaultsKey, class: SPTokenModel.self) as? SPTokenModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setLoginToken(token: SPTokenModel?) {
|
||||||
|
self.token = token
|
||||||
|
UserDefaults.jx_setObject(token, forKey: kSPLoginTokenDefaultsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPLoginManager {
|
||||||
|
///游客登录
|
||||||
|
func requestVisitorLogin(completer: (() -> Void)?) {
|
||||||
|
if isRefreshingToken {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isRefreshingToken = true
|
||||||
|
// var loding = true
|
||||||
|
|
||||||
|
let param = SPNetworkParameters(path: "/customer/register")
|
||||||
|
SPNetwork.request(parameters: param) { [weak self] (response: SPNetworkResponse<SPTokenModel>) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let token = response.data {
|
||||||
|
self.setLoginToken(token: token)
|
||||||
|
}
|
||||||
|
self.isRefreshingToken = false
|
||||||
|
// loding = false
|
||||||
|
completer?()
|
||||||
|
}
|
||||||
|
|
||||||
|
// while loding {
|
||||||
|
// RunLoop.current.run(mode: .default, before: Date.distantFuture)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
39
ShortPlay/Libs/Login/SPTokenModel.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// SPTokenModel.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class SPTokenModel: SPModel, SmartCodable, NSSecureCoding {
|
||||||
|
|
||||||
|
var token: String?
|
||||||
|
var customer_id: String?
|
||||||
|
var auto_login: Int?
|
||||||
|
|
||||||
|
required init() { }
|
||||||
|
|
||||||
|
static var supportsSecureCoding: Bool {
|
||||||
|
get {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func encode(with coder: NSCoder) {
|
||||||
|
coder.encode(token, forKey: "token")
|
||||||
|
coder.encode(customer_id, forKey: "customer_id")
|
||||||
|
coder.encode(auto_login, forKey: "auto_login")
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
token = coder.decodeObject(of: NSString.self, forKey: "token") as? String
|
||||||
|
customer_id = coder.decodeObject(of: NSString.self, forKey: "customer_id") as? String
|
||||||
|
auto_login = coder.decodeObject(of: NSNumber.self, forKey: "auto_login")?.intValue
|
||||||
|
}
|
||||||
|
}
|
147
ShortPlay/Libs/Player/SPPlayer.swift
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
//
|
||||||
|
// SPPlayer.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ZFPlayer
|
||||||
|
|
||||||
|
@objc protocol SPPlayerDelegate {
|
||||||
|
///更新当前总进度
|
||||||
|
// @objc optional func sp_onDurationUpdate(_ player: SPPlayer, duration: Int)
|
||||||
|
//
|
||||||
|
// ///更新当前进度
|
||||||
|
// @objc optional func sp_onCurrentPositionUpdate(_ player: SPPlayer, position: Int)
|
||||||
|
|
||||||
|
///播放时间发生变化
|
||||||
|
@objc optional func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int)
|
||||||
|
|
||||||
|
///显示首帧
|
||||||
|
@objc optional func sp_firstRenderedStart(_ player: SPPlayer)
|
||||||
|
|
||||||
|
///播放完成
|
||||||
|
@objc optional func sp_playCompletion(_ player: SPPlayer)
|
||||||
|
|
||||||
|
///缓冲完成
|
||||||
|
@objc optional func sp_playLoadingEnd(_ player: SPPlayer)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SPPlayer: NSObject {
|
||||||
|
|
||||||
|
weak var delegate: SPPlayerDelegate?
|
||||||
|
|
||||||
|
private(set) lazy var isPlaying = false
|
||||||
|
|
||||||
|
///总进度
|
||||||
|
var duration: Int {
|
||||||
|
return Int(self.player.totalTime)
|
||||||
|
}
|
||||||
|
///当前进度
|
||||||
|
var currentPosition: Int {
|
||||||
|
return Int(self.player.currentTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerView: UIView? {
|
||||||
|
didSet {
|
||||||
|
playerView?.addSubview(player.view)
|
||||||
|
player.view.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var player: ZFAVPlayerManager = {
|
||||||
|
let player = ZFAVPlayerManager()
|
||||||
|
return player
|
||||||
|
}()
|
||||||
|
|
||||||
|
var isLoop = true
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
player.scalingMode = .aspectFill
|
||||||
|
sp_addAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPlayUrl(url: String) {
|
||||||
|
let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url))
|
||||||
|
self.player.assetURL = proxyURL
|
||||||
|
|
||||||
|
// self.player.assetURL = URL(string: url)
|
||||||
|
self.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
///准备播放
|
||||||
|
func prepare() {
|
||||||
|
self.player.prepareToPlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
self.isPlaying = false
|
||||||
|
player.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
self.isPlaying = true
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
///暂停
|
||||||
|
func pause() {
|
||||||
|
self.isPlaying = false
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
func seekToTime(toTime: Int) {
|
||||||
|
// self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE)
|
||||||
|
self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPPlayer {
|
||||||
|
|
||||||
|
private func sp_addAction() {
|
||||||
|
//进度发生变化
|
||||||
|
player.playerPlayTimeChanged = { [weak self] (asset, currentTime, duration) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.sp_playTimeChanged?(self, currentTime: Int(currentTime), duration: Int(duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
//播放状态
|
||||||
|
player.playerPlayStateChanged = { [weak self] (asset, playState) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if playState == .playStatePlaying, !isPlaying {
|
||||||
|
self.pause()
|
||||||
|
}
|
||||||
|
spLog(message: "播放状态====\(playState)")
|
||||||
|
}
|
||||||
|
|
||||||
|
//加载状态
|
||||||
|
player.playerLoadStateChanged = { [weak self] (asset, loadState) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if loadState == .playable, !isPlaying {
|
||||||
|
self.pause()
|
||||||
|
}
|
||||||
|
spLog(message: "加载状态====\(loadState)")
|
||||||
|
}
|
||||||
|
|
||||||
|
//错误信息
|
||||||
|
player.playerPlayFailed = { [weak self] (asset, error) in
|
||||||
|
spLog(message: "错误信息====\(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
//播放结束
|
||||||
|
player.playerDidToEnd = { [weak self] (asset) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if isLoop {
|
||||||
|
self.player.replay()
|
||||||
|
} else {
|
||||||
|
self.delegate?.sp_playCompletion?(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
61
ShortPlay/Libs/SPLocalizedManager/SPLocalizedManager.swift
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// SPLocalizedManager.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPLocalizedManager: NSObject {
|
||||||
|
static let shared = SPLocalizedManager()
|
||||||
|
|
||||||
|
private let userDefaultsKey = "AppLocalized"
|
||||||
|
|
||||||
|
// 获取当前语言代码(如果用户未手动设置,则返回系统语言)
|
||||||
|
var currentLocalizedKey: String {
|
||||||
|
get {
|
||||||
|
// return UserDefaults.standard.string(forKey: userDefaultsKey) ?? Locale.preferredLanguages.first ?? "en"
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
UserDefaults.standard.set(newValue, forKey: userDefaultsKey)
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
NotificationCenter.default.post(name: SPLocalizedManager.localizedDidChange, object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否跟随系统
|
||||||
|
var isFollowingSystem: Bool {
|
||||||
|
return UserDefaults.standard.string(forKey: userDefaultsKey) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 还原为系统默认语言
|
||||||
|
func resetToSystemLanguage() {
|
||||||
|
UserDefaults.standard.removeObject(forKey: userDefaultsKey)
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本地化字符串
|
||||||
|
func localizedString(forKey key: String, tableName: String? = nil) -> String {
|
||||||
|
if let selectedLanguage = UserDefaults.standard.string(forKey: userDefaultsKey),
|
||||||
|
let bundlePath = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj"),
|
||||||
|
let bundle = Bundle(path: bundlePath) {
|
||||||
|
return bundle.localizedString(forKey: key, value: nil, table: tableName)
|
||||||
|
} else {
|
||||||
|
return NSLocalizedString(key, tableName: tableName, bundle: .main, value: "", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SPLocalizedManager {
|
||||||
|
|
||||||
|
static let localizedDidChange = Notification.Name(rawValue: "SPLocalizedManager.localizedDidChange")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
var localized: String {
|
||||||
|
return SPLocalizedManager.shared.localizedString(forKey: self)
|
||||||
|
}
|
||||||
|
}
|
44
ShortPlay/Libs/User/SPUserInfo.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// SPUserInfo.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class SPUserInfo: SPModel, SmartCodable, NSSecureCoding {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
required init() { }
|
||||||
|
|
||||||
|
static var supportsSecureCoding: Bool {
|
||||||
|
get {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func encode(with coder: NSCoder) {
|
||||||
|
// coder.encode(id, forKey: "id")
|
||||||
|
// coder.encode(phone, forKey: "phone")
|
||||||
|
// coder.encode(userToken, forKey: "userToken")
|
||||||
|
// coder.encode(ipAddress, forKey: "ipAddress")
|
||||||
|
// coder.encode(audioNum, forKey: "audioNum")
|
||||||
|
// coder.encode(audioSeconds, forKey: "audioSeconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
// id = coder.decodeObject(of: NSString.self, forKey: "id") as? String
|
||||||
|
// phone = coder.decodeObject(of: NSString.self, forKey: "phone") as? String
|
||||||
|
// userToken = coder.decodeObject(of: NSString.self, forKey: "userToken") as? String
|
||||||
|
// ipAddress = coder.decodeObject(of: NSString.self, forKey: "ipAddress") as? String
|
||||||
|
// audioNum = coder.decodeObject(of: NSNumber.self, forKey: "audioNum")?.intValue
|
||||||
|
// audioSeconds = coder.decodeObject(of: NSNumber.self, forKey: "audioSeconds")?.intValue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
6
ShortPlay/Source/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
6
ShortPlay/Source/Assets.xcassets/TabBar/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
23
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
25
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 636 B |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01_selected.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.0 KiB |
25
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.3 KiB |
23
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
25
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 779 B |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
23
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_03_selected.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
25
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 984 B |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.0 KiB |
23
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_04_selected.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
23
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
25
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"template-rendering-intent" : "original"
|
||||||
|
}
|
||||||
|
}
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@2x.png
vendored
Normal file
After Width: | Height: | Size: 799 B |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05_selected.imageset/Frame@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
25
ShortPlay/Source/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
25
ShortPlay/Source/Info.plist
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>Default Configuration</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
13
ShortPlay/Source/ShortPlay-Bridging-Header.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// ShortPlay-Bridging-Header.h
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "JXUUID.h"
|
||||||
|
#import <YYKit/YYKit.h>
|
||||||
|
#import <ZFPlayer.h>
|
||||||
|
#import <Toast.h>
|
||||||
|
#import "NSUserDefaults+JXAdd.h"
|
||||||
|
#import <KTVHTTPCache.h>
|
11
ShortPlay/Source/en.lproj/Localizable.strings
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Localizable.strings
|
||||||
|
ShortPlay
|
||||||
|
|
||||||
|
Created by 曾觉新 on 2025/4/8.
|
||||||
|
英语
|
||||||
|
*/
|
||||||
|
|
||||||
|
"Home" = "Home";
|
||||||
|
"For You" = "For You";
|
||||||
|
"Error" = "Error";
|
86
ShortPlay/Thirdparty/JXTransition/JXBaseAnimatedTransition.swift
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// JXBaseAnimatedTransition.swift
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/10/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class JXBaseAnimatedTransition: NSObject {
|
||||||
|
|
||||||
|
open var isHideTabBar = false
|
||||||
|
|
||||||
|
open var transitionContext: UIViewControllerContextTransitioning?
|
||||||
|
open weak var containerView: UIView?
|
||||||
|
open weak var fromViewController: UIViewController?
|
||||||
|
open weak var toViewController: UIViewController?
|
||||||
|
|
||||||
|
open var contentView: UIView?
|
||||||
|
|
||||||
|
// open var isScale = false
|
||||||
|
// open var shadowView: UIView!
|
||||||
|
// open var isHideTabBar = false
|
||||||
|
// open var contentView: UIView?
|
||||||
|
|
||||||
|
|
||||||
|
open func getCapture(with view: UIView) -> UIImage? {
|
||||||
|
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0)
|
||||||
|
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
|
||||||
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
|
UIGraphicsEndImageContext()
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXBaseAnimatedTransition: UIViewControllerAnimatedTransitioning {
|
||||||
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
return TimeInterval(UINavigationController.hideShowBarDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
let containerView = transitionContext.containerView
|
||||||
|
|
||||||
|
let fromVC = transitionContext.viewController(forKey: .from)
|
||||||
|
let toVC = transitionContext.viewController(forKey: .to)
|
||||||
|
|
||||||
|
self.containerView = containerView
|
||||||
|
self.fromViewController = fromVC
|
||||||
|
self.toViewController = toVC
|
||||||
|
self.transitionContext = transitionContext
|
||||||
|
|
||||||
|
animateTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@objc open func animateTransition() {
|
||||||
|
// SubClass Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
public func completeTransition() {
|
||||||
|
guard let transitionContext = self.transitionContext else { return }
|
||||||
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animationDuration() -> TimeInterval {
|
||||||
|
return self.transitionDuration(using: self.transitionContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
fileprivate struct AssociatedKeys {
|
||||||
|
static var defCaptureImage: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
public var jx_captureImage: UIImage? {
|
||||||
|
get {
|
||||||
|
guard let obj = objc_getAssociatedObject(self, &AssociatedKeys.defCaptureImage) else { return nil }
|
||||||
|
return obj as? UIImage
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.defCaptureImage, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
202
ShortPlay/Thirdparty/JXTransition/JXNavigationInteractiveTransition.swift
vendored
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
//
|
||||||
|
// JXNavigationInteractiveTransition.swift
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/10/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class JXNavigationInteractiveTransition: NSObject {
|
||||||
|
|
||||||
|
|
||||||
|
var interactionController: UIPercentDrivenInteractiveTransition?
|
||||||
|
|
||||||
|
var isGesturePush = false
|
||||||
|
|
||||||
|
weak var navigationController: UINavigationController!
|
||||||
|
weak var visibleVC: UIViewController?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension JXNavigationInteractiveTransition {
|
||||||
|
|
||||||
|
@objc func panGestureRecognizerAction(_ sender: UIPanGestureRecognizer) {
|
||||||
|
guard let view = sender.view else { return }
|
||||||
|
|
||||||
|
|
||||||
|
var progress = sender.translation(in: view).x / UIScreen.main.bounds.width
|
||||||
|
let velocity = sender.velocity(in: sender.view)
|
||||||
|
|
||||||
|
|
||||||
|
// 在手势开始的时候判断是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
|
||||||
|
}
|
||||||
|
}
|
72
ShortPlay/Thirdparty/JXTransition/JXPopAnimatedTransition.swift
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// JXPopAnimatedTransition.swift
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/10/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class JXPopAnimatedTransition: JXBaseAnimatedTransition {
|
||||||
|
|
||||||
|
override func animateTransition() {
|
||||||
|
guard let fromView = self.fromViewController?.view else { return }
|
||||||
|
|
||||||
|
var toView = self.toViewController?.view
|
||||||
|
|
||||||
|
if toView == nil { return }
|
||||||
|
|
||||||
|
self.containerView?.insertSubview(toView!, belowSubview: fromView)
|
||||||
|
|
||||||
|
let size = self.containerView?.bounds.size ?? .zero
|
||||||
|
|
||||||
|
self.isHideTabBar = (self.toViewController?.tabBarController != nil) && (self.fromViewController?.hidesBottomBarWhenPushed == true) && (self.toViewController?.jx_captureImage != nil)
|
||||||
|
|
||||||
|
if self.isHideTabBar {
|
||||||
|
let captureView = UIImageView(image: self.toViewController?.jx_captureImage!)
|
||||||
|
|
||||||
|
captureView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||||
|
self.containerView?.insertSubview(captureView, belowSubview: fromView)
|
||||||
|
toView = captureView
|
||||||
|
self.toViewController?.view.isHidden = true
|
||||||
|
self.toViewController?.tabBarController?.tabBar.isHidden = true
|
||||||
|
}
|
||||||
|
self.contentView = toView
|
||||||
|
|
||||||
|
|
||||||
|
fromView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||||
|
toView!.frame = CGRect(x: -(0.3 * size.width), y: 0, width: size.width, height: size.height)
|
||||||
|
|
||||||
|
fromView.layer.shadowColor = UIColor.black.cgColor
|
||||||
|
fromView.layer.shadowOpacity = 0.15
|
||||||
|
fromView.layer.shadowRadius = 3.0
|
||||||
|
|
||||||
|
UIView.animate(withDuration: animationDuration(), delay: 0, options: .curveEaseInOut) {
|
||||||
|
|
||||||
|
fromView.frame = CGRect(x: size.width, y: 0, width: size.width, height: size.height)
|
||||||
|
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
toView!.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||||
|
}else {
|
||||||
|
toView!.transform = .identity
|
||||||
|
}
|
||||||
|
|
||||||
|
} completion: { finish in
|
||||||
|
|
||||||
|
if self.isHideTabBar {
|
||||||
|
if self.contentView != nil {
|
||||||
|
self.contentView?.removeFromSuperview()
|
||||||
|
self.contentView = nil
|
||||||
|
}
|
||||||
|
self.toViewController?.view.isHidden = false
|
||||||
|
if self.toViewController?.navigationController?.children.count == 1 {
|
||||||
|
self.toViewController?.tabBarController?.tabBar.isHidden = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.completeTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
79
ShortPlay/Thirdparty/JXTransition/JXPushAnimatedTransition.swift
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
//
|
||||||
|
// JXPushAnimatedTransition.swift
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/10/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class JXPushAnimatedTransition: JXBaseAnimatedTransition {
|
||||||
|
|
||||||
|
|
||||||
|
override func animateTransition() {
|
||||||
|
guard let toView = self.toViewController?.view else { return }
|
||||||
|
|
||||||
|
var fromView = self.fromViewController?.view
|
||||||
|
|
||||||
|
let size = self.containerView?.bounds.size ?? .zero
|
||||||
|
|
||||||
|
// 解决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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
ShortPlay/Thirdparty/JXTransition/JXTransitionDefine.swift
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// JXTransitionDefine.swift
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/10/11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public func jx_swizzled_instanceMethod(_ prefix: String, oldClass: Swift.AnyClass!, oldSelector: String, newClass: Swift.AnyClass) {
|
||||||
|
let newSelector = prefix + "_" + oldSelector;
|
||||||
|
|
||||||
|
let originalSelector = NSSelectorFromString(oldSelector)
|
||||||
|
let swizzledSelector = NSSelectorFromString(newSelector)
|
||||||
|
|
||||||
|
let originalMethod = class_getInstanceMethod(oldClass, originalSelector)
|
||||||
|
let swizzledMethod = class_getInstanceMethod(newClass, swizzledSelector)
|
||||||
|
|
||||||
|
let isAdd = class_addMethod(oldClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
|
||||||
|
|
||||||
|
if isAdd {
|
||||||
|
class_replaceMethod(newClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
|
||||||
|
}else {
|
||||||
|
method_exchangeImplementations(originalMethod!, swizzledMethod!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - 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()
|
||||||
|
}
|
||||||
|
}
|
16
ShortPlay/Thirdparty/JXTransition/JXTransitionDelegateBridge.swift
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// JXTransitionDelegateBridge.swift
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/10/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class JXTransitionDelegateBridge: NSObject {
|
||||||
|
|
||||||
|
weak var jx_pushDelegate: JXViewControllerPushDelegate?
|
||||||
|
|
||||||
|
weak var jx_popDelegate: JXViewControllerPopDelegate?
|
||||||
|
|
||||||
|
}
|
25
ShortPlay/Thirdparty/JXTransition/UIGestureRecognizer+JXTransition.swift
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// UIGestureRecognizer+JXTransition.swift
|
||||||
|
// YDLive
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/11/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIGestureRecognizer {
|
||||||
|
|
||||||
|
fileprivate struct AssociatedKeys {
|
||||||
|
static var jxTag: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
var jxTag: String? {
|
||||||
|
get {
|
||||||
|
guard let obj = objc_getAssociatedObject(self, &AssociatedKeys.jxTag) as? String else { return nil }
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.jxTag, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
130
ShortPlay/Thirdparty/JXTransition/UINavigationController+JXTransition.swift
vendored
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
//
|
||||||
|
// UINavigationController+JXTransition.swift
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2022/10/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
extension UINavigationController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
承载手势的视图
|
||||||
|
*/
|
||||||
|
private var gestureView: UIView? {
|
||||||
|
return self.interactivePopGestureRecognizer?.view
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
激活转场动画
|
||||||
|
*/
|
||||||
|
func jx_transitionAwake() {
|
||||||
|
// UIViewController.jxTransitionAwake()
|
||||||
|
|
||||||
|
self.delegate = self.interactiveTransition
|
||||||
|
self.interactivePopGestureRecognizer?.isEnabled = false
|
||||||
|
|
||||||
|
self.gestureView?.addGestureRecognizer(pushGesture)
|
||||||
|
self.gestureView?.addGestureRecognizer(popGesture)
|
||||||
|
self.gestureView?.addGestureRecognizer(edgePopGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension UINavigationController {
|
||||||
|
fileprivate struct AssociatedKeys {
|
||||||
|
static var screenPanGesture: Int?
|
||||||
|
static var panGesture: Int?
|
||||||
|
static var panPushGesture: Int?
|
||||||
|
static var transition: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
var interactiveTransition: JXNavigationInteractiveTransition {
|
||||||
|
get {
|
||||||
|
var transition = objc_getAssociatedObject(self, &AssociatedKeys.transition) as? JXNavigationInteractiveTransition
|
||||||
|
if transition == nil {
|
||||||
|
transition = JXNavigationInteractiveTransition()
|
||||||
|
transition?.navigationController = self
|
||||||
|
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.transition, transition, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
return transition!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
边缘滑动手势
|
||||||
|
*/
|
||||||
|
var edgePopGesture: UIScreenEdgePanGestureRecognizer {
|
||||||
|
get {
|
||||||
|
var panGesture = objc_getAssociatedObject(self, &AssociatedKeys.screenPanGesture) as? UIScreenEdgePanGestureRecognizer
|
||||||
|
if panGesture == nil {
|
||||||
|
panGesture = UIScreenEdgePanGestureRecognizer(target: self.systemTarget, action: self.systemAction)
|
||||||
|
panGesture?.jxTag = "edgePop"
|
||||||
|
panGesture?.edges = .left
|
||||||
|
panGesture?.delegate = self.interactiveTransition
|
||||||
|
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.screenPanGesture, panGesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
return panGesture!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
全屏返回手势
|
||||||
|
*/
|
||||||
|
var popGesture: UIPanGestureRecognizer {
|
||||||
|
get {
|
||||||
|
var panGesture = objc_getAssociatedObject(self, &AssociatedKeys.panGesture) as? UIPanGestureRecognizer
|
||||||
|
if panGesture == nil {
|
||||||
|
panGesture = UIPanGestureRecognizer(target: self.systemTarget, action: self.systemAction)
|
||||||
|
panGesture?.jxTag = "pop"
|
||||||
|
panGesture?.maximumNumberOfTouches = 1
|
||||||
|
panGesture?.delegate = self.interactiveTransition
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.panGesture, panGesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
return panGesture!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
全屏push手势
|
||||||
|
*/
|
||||||
|
var pushGesture: UIPanGestureRecognizer {
|
||||||
|
get {
|
||||||
|
var panGesture = objc_getAssociatedObject(self, &AssociatedKeys.panPushGesture) as? UIPanGestureRecognizer
|
||||||
|
if panGesture == nil {
|
||||||
|
panGesture = UIPanGestureRecognizer(target: self.interactiveTransition, action: #selector(self.interactiveTransition.panGestureRecognizerAction(_:)))
|
||||||
|
panGesture?.jxTag = "push"
|
||||||
|
panGesture?.maximumNumberOfTouches = 1
|
||||||
|
panGesture?.delegate = self.interactiveTransition
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.panPushGesture, panGesture, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
return panGesture!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var systemTarget: Any? {
|
||||||
|
get {
|
||||||
|
let internalTargets = self.interactivePopGestureRecognizer?.value(forKey: "targets") as? [AnyObject]
|
||||||
|
let internamTarget = internalTargets?.first?.value(forKey: "target")
|
||||||
|
return internamTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var systemAction: Selector {
|
||||||
|
return NSSelectorFromString("handleNavigationTransition:")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
是否正在做转场动画
|
||||||
|
*/
|
||||||
|
var jx_isTransitioning: Bool? {
|
||||||
|
return value(forKey: "_isTransitioning") as? Bool
|
||||||
|
}
|
||||||
|
}
|
127
ShortPlay/Thirdparty/JXTransition/UIViewController+JXTransition.swift
vendored
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
20
ShortPlay/Thirdparty/JXUUID/JXUUID.h
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// JXUUID.h
|
||||||
|
// 设备标识符
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2017/8/24.
|
||||||
|
// Copyright © 2017年 曾觉新. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@interface JXUUID : NSObject
|
||||||
|
|
||||||
|
+ (nonnull NSString *)uuid;
|
||||||
|
+ (nonnull NSString *)idfa;
|
||||||
|
/**
|
||||||
|
重新安装app后,会发生变化
|
||||||
|
*/
|
||||||
|
+ (nonnull NSString *)systemUUID;
|
||||||
|
|
||||||
|
@end
|
47
ShortPlay/Thirdparty/JXUUID/JXUUID.m
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// JXUUID.m
|
||||||
|
// 设备标识符
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2017/8/24.
|
||||||
|
// Copyright © 2017年 曾觉新. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "JXUUID.h"
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import "PDKeyChain.h"
|
||||||
|
#import <AdSupport/AdSupport.h>
|
||||||
|
|
||||||
|
static NSString *const uuidKey = @"com.JXUUID";
|
||||||
|
|
||||||
|
@implementation JXUUID
|
||||||
|
|
||||||
|
+ (nonnull NSString *)uuid
|
||||||
|
{
|
||||||
|
static NSString *uuid;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
uuid = [PDKeyChain objectForKey:uuidKey];
|
||||||
|
if (uuid && uuid.length > 0) {
|
||||||
|
} else {
|
||||||
|
uuid = [[NSUUID UUID] UUIDString];
|
||||||
|
[PDKeyChain setObject:uuid forKey:uuidKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
+ (nonnull NSString *)idfa
|
||||||
|
{
|
||||||
|
static NSString *idfa;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
|
||||||
|
});
|
||||||
|
return idfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (nonnull NSString *)systemUUID
|
||||||
|
{
|
||||||
|
return [UIDevice currentDevice].identifierForVendor.UUIDString;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
31
ShortPlay/Thirdparty/JXUUID/PDKeyChain.h
vendored
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// PDKeyChain.h
|
||||||
|
// PDKeyChain
|
||||||
|
//
|
||||||
|
// Created by Panda on 16/8/23.
|
||||||
|
// Copyright © 2016年 v2panda. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
|
||||||
|
@interface PDKeyChain : NSObject
|
||||||
|
/**
|
||||||
|
* 从 KeyChain 中读取存储的数据
|
||||||
|
*
|
||||||
|
* @return NSDictionary
|
||||||
|
*/
|
||||||
|
+ (NSDictionary *)getKeyChainData;
|
||||||
|
|
||||||
|
+ (id)objectForKey:(NSString *)key;
|
||||||
|
+ (void)setObject:(id)object forKey:(NSString *)key;
|
||||||
|
+ (void)removeObjectForKey:(NSString *)key;
|
||||||
|
+ (void)removeAllObjects;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 KeyChain 信息
|
||||||
|
*/
|
||||||
|
+ (void)keyChainDelete;
|
||||||
|
|
||||||
|
@end
|
100
ShortPlay/Thirdparty/JXUUID/PDKeyChain.m
vendored
Executable file
@ -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
|
127
ShortPlay/Thirdparty/JYPageController/Classes/JYPageConfig.swift
vendored
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
//
|
||||||
|
// JYPageConfig.swift
|
||||||
|
// JYPageController
|
||||||
|
//
|
||||||
|
// Created by wang tao on 2022/7/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
@objc enum JYSegmentedItemState: Int {
|
||||||
|
case normal = 0
|
||||||
|
case selected = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public enum JYSegmentedViewAlignment: Int {
|
||||||
|
case left
|
||||||
|
case right
|
||||||
|
case center
|
||||||
|
}
|
||||||
|
|
||||||
|
///IndicatorStyle
|
||||||
|
@objc public enum JYSegmentedViewIndicatorStyle: Int {
|
||||||
|
case none //不显示指示器. indicator hidden
|
||||||
|
case singleLine //指示器下划线宽度等于标题的文字宽度. default: lineWidth = title width
|
||||||
|
case customView //自定义view做指示器,需要设置customIndicator属性. custom 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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
21
ShortPlay/Thirdparty/JYPageController/Classes/JYPageContollerProtocol.swift
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// JYPageContollerProtocol.swift
|
||||||
|
// JYPageController
|
||||||
|
//
|
||||||
|
// Created by wang tao on 2022/10/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
@objc public protocol JYPageChildContollerProtocol where Self: UIViewController {
|
||||||
|
|
||||||
|
///fetch child controller scrollView. 有headerView的时候segmentedView需要悬浮的时候返回子页面中的tableView/collectionView/scrollView
|
||||||
|
@objc optional func fetchChildControllerScrollView() -> UIScrollView?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class JYPlaceHolderController: UIViewController,JYPageChildContollerProtocol {
|
||||||
|
|
||||||
|
}
|
552
ShortPlay/Thirdparty/JYPageController/Classes/JYPageController.swift
vendored
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
//
|
||||||
|
// JYPageController.swift
|
||||||
|
// JYPageController
|
||||||
|
//
|
||||||
|
// Created by wang tao on 2022/7/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
@objc public protocol JYPageControllerDataSource {
|
||||||
|
|
||||||
|
///segmentview frame
|
||||||
|
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect
|
||||||
|
|
||||||
|
///子页面下滚动区域frame
|
||||||
|
func pageController(_ pageController: JYPageController, frameForContainerView container: UIScrollView) -> CGRect
|
||||||
|
|
||||||
|
///第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<NSString, UIViewController>()
|
||||||
|
|
||||||
|
///缓存当前scrollView上展示的vc,用于处理子vc的生命周期逻辑
|
||||||
|
private var displayControllerCache = Dictionary<NSString, UIViewController>()
|
||||||
|
|
||||||
|
///childController scrollView cache
|
||||||
|
private var childScrollViewCache: Dictionary = Dictionary<NSString, UIScrollView?>()
|
||||||
|
|
||||||
|
///menuview frame
|
||||||
|
private var menuViewFrame: CGRect = .zero
|
||||||
|
|
||||||
|
///滚动区域container(scrollView)的frame
|
||||||
|
private var childControllerViewFrame: CGRect = .zero
|
||||||
|
|
||||||
|
///标记scorllView滚动是否由拖拽触发
|
||||||
|
private var scrollByDragging = false
|
||||||
|
|
||||||
|
///当前的偏移量,用来判断向左还是向右滑动
|
||||||
|
private var currentOffsetX: CGFloat = 0
|
||||||
|
|
||||||
|
///有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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
18
ShortPlay/Thirdparty/JYPageController/Classes/JYScrollView.swift
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// JYScrollView.swift
|
||||||
|
// JYPageController
|
||||||
|
//
|
||||||
|
// Created by wang tao on 2022/10/12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public class JYScrollView: UITableView,UIGestureRecognizerDelegate {
|
||||||
|
|
||||||
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
return gestureRecognizer.isKind(of: UIPanGestureRecognizer.classForCoder()) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.classForCoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
682
ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedView.swift
vendored
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
146
ShortPlay/Thirdparty/JYPageController/Classes/JYSegmentedViewItem.swift
vendored
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
//
|
||||||
|
// JYPageMenuItem.swift
|
||||||
|
// JYPageController
|
||||||
|
//
|
||||||
|
// Created by wang tao on 2022/7/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objc enum JYSegmentedViewItemType: Int {
|
||||||
|
case text
|
||||||
|
case customView
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc protocol JYSegmentedViewItemDelegate {
|
||||||
|
func segmentedItemDidSelected(_ item: JYSegmentedViewItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
class JYSegmentedViewItem: UIView {
|
||||||
|
|
||||||
|
weak open var delegate: JYSegmentedViewItemDelegate?
|
||||||
|
|
||||||
|
var badgeView: UIView?
|
||||||
|
|
||||||
|
var customView: UIView?
|
||||||
|
|
||||||
|
var type: JYSegmentedViewItemType = .text
|
||||||
|
|
||||||
|
var normalColorRed: CGFloat = 0, normalColorGreen: CGFloat = 0, normalColorBlue: CGFloat = 0, normalColorAlpha: CGFloat = 0
|
||||||
|
|
||||||
|
var selectedColorRed: CGFloat = 0, selectedColorGreen: CGFloat = 0, selectedColorBlue: CGFloat = 0, selectedColorAlpha: CGFloat = 0
|
||||||
|
|
||||||
|
var badgeViewWidth: CGFloat {
|
||||||
|
get {
|
||||||
|
return badgeView?.frame.width ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var badgeViewHeight: CGFloat {
|
||||||
|
get {
|
||||||
|
return badgeView?.frame.height ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasBadgeView: Bool {
|
||||||
|
get {
|
||||||
|
if badgeView == nil {
|
||||||
|
return false
|
||||||
|
}else{
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if selected {
|
||||||
|
textLabel.textColor = config.selectedTitleColor
|
||||||
|
}else{
|
||||||
|
textLabel.textColor = config.normalTitleColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var font: UIFont? {
|
||||||
|
didSet {
|
||||||
|
textLabel.font = font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var textColor: UIColor? {
|
||||||
|
didSet {
|
||||||
|
textLabel.textColor = textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text: String? {
|
||||||
|
get {
|
||||||
|
return textLabel.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var config: JYPageConfig = JYPageConfig() {
|
||||||
|
didSet {
|
||||||
|
config.normalTitleColor.getRed(&normalColorRed, green: &normalColorGreen, blue: &normalColorBlue, alpha: &normalColorAlpha)
|
||||||
|
config.selectedTitleColor.getRed(&selectedColorRed, green: &selectedColorGreen, blue: &selectedColorBlue, alpha: &selectedColorAlpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rate: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
let red: CGFloat = normalColorRed + (selectedColorRed - normalColorRed) * rate
|
||||||
|
let green: CGFloat = normalColorGreen + (selectedColorGreen - normalColorGreen) * rate
|
||||||
|
let blue: CGFloat = normalColorBlue + (selectedColorBlue - normalColorBlue) * rate
|
||||||
|
let alpha: CGFloat = normalColorAlpha + (selectedColorAlpha - normalColorAlpha) * rate
|
||||||
|
textLabel.textColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
let tap = UITapGestureRecognizer.init(target: self, action: #selector(tapAtion(_:)))
|
||||||
|
addGestureRecognizer(tap)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init(text: String) {
|
||||||
|
self.init(frame: .zero)
|
||||||
|
|
||||||
|
addSubview(textLabel)
|
||||||
|
textLabel.text = text
|
||||||
|
type = .text
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init(customItemView: UIView) {
|
||||||
|
self.init(frame: .zero)
|
||||||
|
|
||||||
|
addSubview(customItemView)
|
||||||
|
customView = customItemView
|
||||||
|
type = .customView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
textLabel.frame = bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Private
|
||||||
|
@objc private func tapAtion(_ gesture: UIGestureRecognizer) {
|
||||||
|
if !selected {
|
||||||
|
delegate?.segmentedItemDidSelected(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var textLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
577
ShortPlay/Thirdparty/ZKCycleScrollView-Swift/ZKCycleScrollView.swift
vendored
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
//
|
||||||
|
// ZKCycleScrollView.swift
|
||||||
|
// ZKCycleScrollViewDemo
|
||||||
|
//
|
||||||
|
// Created by bestdew on 2019/3/8.
|
||||||
|
// Copyright © 2019 bestdew. All rights reserved.
|
||||||
|
//
|
||||||
|
// d*##$.
|
||||||
|
// zP"""""$e. $" $o
|
||||||
|
//4$ '$ $" $
|
||||||
|
//'$ '$ J$ $F
|
||||||
|
// 'b $k $> $
|
||||||
|
// $k $r J$ d$
|
||||||
|
// '$ $ $" $~
|
||||||
|
// '$ "$ '$E $
|
||||||
|
// $ $L $" $F ...
|
||||||
|
// $. 4B $ $$$*"""*b
|
||||||
|
// '$ $. $$ $$ $F
|
||||||
|
// "$ R$ $F $" $
|
||||||
|
// $k ?$ u* dF .$
|
||||||
|
// ^$. $$" z$ u$$$$e
|
||||||
|
// #$b $E.dW@e$" ?$
|
||||||
|
// #$ .o$$# d$$$$c ?F
|
||||||
|
// $ .d$$#" . zo$> #$r .uF
|
||||||
|
// $L .u$*" $&$$$k .$$d$$F
|
||||||
|
// $$" ""^"$$$P"$P9$
|
||||||
|
// JP .o$$$$u:$P $$
|
||||||
|
// $ ..ue$" "" $"
|
||||||
|
// d$ $F $
|
||||||
|
// $$ ....udE 4B
|
||||||
|
// #$ """"` $r @$
|
||||||
|
// ^$L '$ $F
|
||||||
|
// RN 4N $
|
||||||
|
// *$b d$
|
||||||
|
// $$k $F
|
||||||
|
// $$b $F
|
||||||
|
// $"" $F
|
||||||
|
// '$ $
|
||||||
|
// $L $
|
||||||
|
// '$ $
|
||||||
|
// $ $
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public typealias ZKCycleScrollViewCell = UICollectionViewCell
|
||||||
|
|
||||||
|
public enum ZKScrollDirection: Int {
|
||||||
|
case horizontal
|
||||||
|
case vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public protocol ZKCycleScrollViewDataSource: NSObjectProtocol {
|
||||||
|
/// Return number of pages
|
||||||
|
func numberOfItems(in cycleScrollView: ZKCycleScrollView) -> Int
|
||||||
|
/// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndex:
|
||||||
|
func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, cellForItemAt index: Int) -> ZKCycleScrollViewCell
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public protocol ZKCycleScrollViewDelegate: NSObjectProtocol {
|
||||||
|
/// Called when the cell is clicked
|
||||||
|
@objc optional func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, didSelectItemAt index: Int)
|
||||||
|
/// Called when the offset changes. The progress range is from 0 to the maximum index value, which means the progress value for a round of scrolling
|
||||||
|
@objc optional func cycleScrollViewDidScroll(_ cycleScrollView: ZKCycleScrollView, progress: Double)
|
||||||
|
/// Called when scrolling to a new index page
|
||||||
|
@objc optional func cycleScrollView(_ cycleScrollView: ZKCycleScrollView, didScrollFromIndex fromIndex: Int, toIndex: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBDesignable open class ZKCycleScrollView: UIView {
|
||||||
|
|
||||||
|
@IBOutlet open weak var delegate: ZKCycleScrollViewDelegate?
|
||||||
|
@IBOutlet open weak var dataSource: ZKCycleScrollViewDataSource?
|
||||||
|
|
||||||
|
|
||||||
|
var currentIdx = 0
|
||||||
|
var stepLength: CGFloat? // 自定义每个时间间隔移动的步长
|
||||||
|
|
||||||
|
#if TARGET_INTERFACE_BUILDER
|
||||||
|
@IBInspectable open var scrollDirection: Int = 0
|
||||||
|
#else
|
||||||
|
/// default horizontal. scroll direction
|
||||||
|
open var scrollDirection: ZKScrollDirection = .horizontal {
|
||||||
|
didSet {
|
||||||
|
switch scrollDirection {
|
||||||
|
case .vertical:
|
||||||
|
flowLayout?.scrollDirection = .vertical
|
||||||
|
default:
|
||||||
|
flowLayout?.scrollDirection = .horizontal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
/// default 3.f. automatic scroll time interval
|
||||||
|
@IBInspectable open var autoScrollInterval: TimeInterval = 3 {
|
||||||
|
didSet {
|
||||||
|
addTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@IBInspectable open var isAutoScroll: Bool = true {
|
||||||
|
didSet {
|
||||||
|
addTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// default true. turn off any dragging temporarily
|
||||||
|
@IBInspectable open var allowsDragging: Bool = true {
|
||||||
|
didSet {
|
||||||
|
collectionView.isScrollEnabled = allowsDragging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// default the view size
|
||||||
|
@IBInspectable open var itemSize: CGSize = CGSize.zero {
|
||||||
|
didSet {
|
||||||
|
itemSizeFlag = true
|
||||||
|
flowLayout.itemSize = itemSize
|
||||||
|
flowLayout.headerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
|
||||||
|
flowLayout.footerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// default 0.0
|
||||||
|
@IBInspectable open var itemSpacing: CGFloat = 0.0 {
|
||||||
|
didSet {
|
||||||
|
flowLayout.minimumLineSpacing = itemSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// default 1.f(no scaling), it ranges from 0.f to 1.f
|
||||||
|
@IBInspectable open var itemZoomScale: CGFloat = 1.0 {
|
||||||
|
didSet {
|
||||||
|
flowLayout.zoomScale = itemZoomScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBInspectable open var hidesPageControl: Bool = false {
|
||||||
|
didSet {
|
||||||
|
pageControl?.isHidden = hidesPageControl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@IBInspectable open var pageIndicatorTintColor: UIColor = UIColor.gray {
|
||||||
|
didSet {
|
||||||
|
pageControl?.pageIndicatorTintColor = pageIndicatorTintColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@IBInspectable open var currentPageIndicatorTintColor: UIColor = UIColor.white {
|
||||||
|
didSet {
|
||||||
|
pageControl?.currentPageIndicatorTintColor = currentPageIndicatorTintColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// current page index
|
||||||
|
open var pageIndex: Int {
|
||||||
|
return changeIndex(currentIndex())
|
||||||
|
}
|
||||||
|
/// current content offset
|
||||||
|
open var contentOffset: CGPoint {
|
||||||
|
let num = CGFloat(numberOfAddedCells() / 2)
|
||||||
|
switch scrollDirection {
|
||||||
|
case .vertical:
|
||||||
|
return CGPoint(x: 0.0, y: max(0.0, collectionView.contentOffset.y - (flowLayout.itemSize.height + flowLayout.minimumLineSpacing) * num))
|
||||||
|
default:
|
||||||
|
return CGPoint(x: max(0.0, collectionView.contentOffset.x - (flowLayout.itemSize.width + flowLayout.minimumLineSpacing) * num), y: 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// infinite cycle
|
||||||
|
@IBInspectable open private(set) var isInfiniteLoop: Bool = true
|
||||||
|
/// load completed callback
|
||||||
|
open var loadCompletion: (() -> Void)? = nil
|
||||||
|
open var pageControlFrame: CGRect?
|
||||||
|
var pageControl: UIPageControl!
|
||||||
|
var collectionView: UICollectionView!
|
||||||
|
private var flowLayout: ZKCycleScrollViewFlowLayout!
|
||||||
|
private var timer: Timer?
|
||||||
|
private var numberOfItems: Int = 0
|
||||||
|
private var fromIndex: Int = 0
|
||||||
|
private var itemSizeFlag: Bool = false
|
||||||
|
private var indexOffset: Int = 0
|
||||||
|
private var configuredFlag: Bool = false
|
||||||
|
private var tempIndex: Int = 0
|
||||||
|
|
||||||
|
// MARK: - Open Func
|
||||||
|
open func register(_ cellClass: AnyClass?, forCellWithReuseIdentifier identifier: String) {
|
||||||
|
collectionView.register(cellClass, forCellWithReuseIdentifier: identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func register(_ nib: UINib?, forCellWithReuseIdentifier identifier: String) {
|
||||||
|
collectionView.register(nib, forCellWithReuseIdentifier: identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func dequeueReusableCell(withReuseIdentifier identifier: String, for index: Int) -> ZKCycleScrollViewCell {
|
||||||
|
let indexPath = IndexPath(item: changeIndex(index), section: 0)
|
||||||
|
return collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func reloadData() {
|
||||||
|
removeTimer()
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
collectionView.performBatchUpdates(nil) { _ in
|
||||||
|
self.configuration()
|
||||||
|
self.loadCompletion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call -beginUpdates and -endUpdates to update layout
|
||||||
|
/// Allows multiple scrollDirection/itemSize/itemSpacing/itemZoomScale to be set simultaneously.
|
||||||
|
open func beginUpdates() {
|
||||||
|
tempIndex = pageIndex
|
||||||
|
removeTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
open func endUpdates() {
|
||||||
|
flowLayout.invalidateLayout()
|
||||||
|
scrollToItem(at: tempIndex, animated: false)
|
||||||
|
addTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll to page
|
||||||
|
open func scrollToItem(at index: Int, animated: Bool) {
|
||||||
|
let num = numberOfAddedCells()
|
||||||
|
guard index >= 0 && index <= numberOfItems - 1 - num else {
|
||||||
|
print("⚠️attempt to scroll to invalid index:\(index)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removeTimer()
|
||||||
|
let idx = index + num / 2
|
||||||
|
let position = scrollPosition()
|
||||||
|
let indexPath = IndexPath(item: idx, section: 0)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: position, animated: animated)
|
||||||
|
|
||||||
|
addTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the visible cell object at the specified index
|
||||||
|
open func cellForItem(at index: Int) -> ZKCycleScrollViewCell? {
|
||||||
|
let num = numberOfAddedCells()
|
||||||
|
guard index >= 0 && index <= numberOfItems - 1 - num else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let idx = index + num / 2
|
||||||
|
let indexPath = IndexPath(item: idx, section: 0)
|
||||||
|
let cell = collectionView.cellForItem(at: indexPath)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
public init(frame: CGRect, shouldInfiniteLoop infiniteLoop: Bool? = nil) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
isInfiniteLoop = infiniteLoop ?? true
|
||||||
|
initialization()
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
initialization()
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
if itemSizeFlag {
|
||||||
|
flowLayout.itemSize = itemSize
|
||||||
|
flowLayout.headerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
|
||||||
|
flowLayout.footerReferenceSize = CGSize(width: (bounds.width - itemSize.width) / 2, height: (bounds.height - itemSize.height) / 2)
|
||||||
|
} else {
|
||||||
|
flowLayout.itemSize = bounds.size
|
||||||
|
flowLayout.headerReferenceSize = CGSize.zero
|
||||||
|
flowLayout.footerReferenceSize = CGSize.zero
|
||||||
|
}
|
||||||
|
collectionView.frame = bounds
|
||||||
|
if let pageControlFrame = pageControlFrame {
|
||||||
|
pageControl.frame = pageControlFrame
|
||||||
|
} else {
|
||||||
|
pageControl.frame = CGRect(x: 0.0, y: bounds.height - 15.0, width: bounds.width, height: 15.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func willMove(toSuperview newSuperview: UIView?) {
|
||||||
|
if newSuperview == nil { removeTimer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func setValue(_ value: Any?, forUndefinedKey key: String) {
|
||||||
|
guard key == "scrollDirection" else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let direction = value as! Int
|
||||||
|
if direction == 1 {
|
||||||
|
scrollDirection = .vertical
|
||||||
|
} else {
|
||||||
|
scrollDirection = .horizontal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
collectionView.delegate = nil
|
||||||
|
collectionView.dataSource = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Func
|
||||||
|
private func initialization() {
|
||||||
|
flowLayout = ZKCycleScrollViewFlowLayout()
|
||||||
|
flowLayout.minimumLineSpacing = 0
|
||||||
|
flowLayout.minimumInteritemSpacing = 0
|
||||||
|
flowLayout.scrollDirection = .horizontal
|
||||||
|
|
||||||
|
collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: flowLayout)
|
||||||
|
collectionView.backgroundColor = nil
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.scrollsToTop = false
|
||||||
|
collectionView.bounces = false
|
||||||
|
collectionView.showsVerticalScrollIndicator = false
|
||||||
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
|
addSubview(collectionView)
|
||||||
|
|
||||||
|
pageControl = UIPageControl()
|
||||||
|
pageControl.isEnabled = false
|
||||||
|
pageControl.hidesForSinglePage = true
|
||||||
|
pageControl.pageIndicatorTintColor = UIColor.gray
|
||||||
|
pageControl.currentPageIndicatorTintColor = UIColor.white
|
||||||
|
addSubview(pageControl);
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.configuration()
|
||||||
|
self.loadCompletion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configuration() {
|
||||||
|
fromIndex = 0
|
||||||
|
indexOffset = 0
|
||||||
|
configuredFlag = false
|
||||||
|
|
||||||
|
guard numberOfItems > 1 else { return }
|
||||||
|
|
||||||
|
let position = scrollPosition()
|
||||||
|
if isInfiniteLoop {
|
||||||
|
let indexPath = IndexPath(item: 2, section: 0)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
addTimer()
|
||||||
|
updatePageControl()
|
||||||
|
|
||||||
|
configuredFlag = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTimer() {
|
||||||
|
removeTimer()
|
||||||
|
|
||||||
|
if numberOfItems < 2 || !isAutoScroll || autoScrollInterval <= 0.0 { return }
|
||||||
|
timer = Timer.scheduledTimer(timeInterval: autoScrollInterval, target: YYWeakProxy(target: self), selector: #selector(automaticScroll), userInfo: nil, repeats: true)
|
||||||
|
RunLoop.main.add(timer!, forMode: .common)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePageControl() {
|
||||||
|
let num = numberOfAddedCells()
|
||||||
|
pageControl.currentPage = 0
|
||||||
|
pageControl.numberOfPages = max(0, numberOfItems - num)
|
||||||
|
pageControl.isHidden = (hidesPageControl || pageControl.numberOfPages < 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func numberOfAddedCells() -> Int {
|
||||||
|
return isInfiniteLoop ? 4 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func automaticScroll() {
|
||||||
|
var index = currentIndex() + 1
|
||||||
|
if !isInfiniteLoop && index >= numberOfItems {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if let stepLength = stepLength {
|
||||||
|
let offsetX = self.collectionView.contentOffset.x
|
||||||
|
let width = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
|
||||||
|
self.collectionView.contentOffset = CGPoint(x: offsetX+stepLength, y: self.contentOffset.y)
|
||||||
|
|
||||||
|
let position = scrollPosition()
|
||||||
|
if self.currentIndex() == 1 {
|
||||||
|
let indexPath = IndexPath(item: numberOfItems - 3, section: 0)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
|
||||||
|
} else if self.currentIndex() == numberOfItems - 2 {
|
||||||
|
let offsetx = (width)*CGFloat(2) - width*0.5
|
||||||
|
self.collectionView.contentOffset = CGPoint(x: offsetx, y: self.contentOffset.y)
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let position = scrollPosition()
|
||||||
|
let indexPath = IndexPath(item: index, section: 0)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: position, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeTimer() {
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scrollPosition() -> UICollectionView.ScrollPosition {
|
||||||
|
switch scrollDirection {
|
||||||
|
case .vertical:
|
||||||
|
return .centeredVertically
|
||||||
|
default:
|
||||||
|
return .centeredHorizontally
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentIndex() -> Int {
|
||||||
|
guard numberOfItems > 0 else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 0
|
||||||
|
var minimumIndex = 0
|
||||||
|
var maximumIndex = numberOfItems - 1
|
||||||
|
|
||||||
|
if numberOfItems == 1 {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
if isInfiniteLoop {
|
||||||
|
minimumIndex = 1
|
||||||
|
maximumIndex = numberOfItems - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
switch scrollDirection {
|
||||||
|
case .vertical:
|
||||||
|
let height = flowLayout.itemSize.height + flowLayout.minimumLineSpacing
|
||||||
|
index = Int((collectionView.contentOffset.y + height / 2) / height)
|
||||||
|
default:
|
||||||
|
let width = flowLayout.itemSize.width + flowLayout.minimumLineSpacing
|
||||||
|
index = Int((collectionView.contentOffset.x + width / 2) / width)
|
||||||
|
}
|
||||||
|
return min(maximumIndex, max(minimumIndex, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func changeIndex(_ index: Int) -> Int {
|
||||||
|
guard isInfiniteLoop && numberOfItems > 1 else {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = index
|
||||||
|
|
||||||
|
if index == 0 {
|
||||||
|
idx = numberOfItems - 6
|
||||||
|
} else if index == 1 {
|
||||||
|
idx = numberOfItems - 5
|
||||||
|
} else if index == numberOfItems - 2 {
|
||||||
|
idx = 0
|
||||||
|
} else if index == numberOfItems - 1 {
|
||||||
|
idx = 1
|
||||||
|
} else {
|
||||||
|
idx = index - 2
|
||||||
|
}
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ZKCycleScrollView: UICollectionViewDelegate {
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
|
||||||
|
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didSelectItemAt:))) {
|
||||||
|
removeTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
|
||||||
|
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didSelectItemAt:))) {
|
||||||
|
addTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didSelectItemAt:))) {
|
||||||
|
delegate.cycleScrollView!(self, didSelectItemAt: changeIndex(indexPath.item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
pageControl.currentPage = pageIndex
|
||||||
|
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollViewDidScroll(_:progress:))) {
|
||||||
|
var total: CGFloat = 0.0
|
||||||
|
var offset: CGFloat = 0.0
|
||||||
|
let num = numberOfAddedCells()
|
||||||
|
|
||||||
|
switch scrollDirection {
|
||||||
|
case .vertical:
|
||||||
|
total = CGFloat(numberOfItems - 1 - num) * (flowLayout.itemSize.height + flowLayout.minimumLineSpacing)
|
||||||
|
offset = contentOffset.y.truncatingRemainder(dividingBy:((flowLayout.itemSize.height + flowLayout.minimumLineSpacing) * CGFloat(numberOfItems - num)))
|
||||||
|
default:
|
||||||
|
total = CGFloat(numberOfItems - 1 - num) * (flowLayout.itemSize.width + flowLayout.minimumLineSpacing)
|
||||||
|
offset = contentOffset.x.truncatingRemainder(dividingBy:((flowLayout.itemSize.width + flowLayout.minimumLineSpacing) * CGFloat(numberOfItems - num)))
|
||||||
|
}
|
||||||
|
let percent = Double(offset / total)
|
||||||
|
let progress = percent * Double(numberOfItems - 1 - num)
|
||||||
|
delegate.cycleScrollViewDidScroll!(self, progress: progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
removeTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
if let _ = stepLength {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
self.addTimer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
|
let index = currentIndex()
|
||||||
|
|
||||||
|
if isInfiniteLoop {
|
||||||
|
let position = scrollPosition()
|
||||||
|
if index == 1 {
|
||||||
|
let indexPath = IndexPath(item: numberOfItems - 3, section: 0)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
|
||||||
|
} else if index == numberOfItems - 2 {
|
||||||
|
let indexPath = IndexPath(item: 2, section: 0)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: position, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let toIndex = changeIndex(index)
|
||||||
|
if let delegate = delegate, delegate.responds(to: #selector(ZKCycleScrollViewDelegate.cycleScrollView(_:didScrollFromIndex:toIndex:))) {
|
||||||
|
delegate.cycleScrollView!(self, didScrollFromIndex: fromIndex, toIndex: toIndex)
|
||||||
|
}
|
||||||
|
fromIndex = toIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
|
guard pageIndex == fromIndex else { return }
|
||||||
|
|
||||||
|
let sum = velocity.x + velocity.y
|
||||||
|
if sum > 0 {
|
||||||
|
indexOffset = 1
|
||||||
|
} else if sum < 0 {
|
||||||
|
indexOffset = -1
|
||||||
|
} else {
|
||||||
|
indexOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
let position = scrollPosition()
|
||||||
|
var index = currentIndex() + indexOffset
|
||||||
|
index = max(0, index)
|
||||||
|
index = min(numberOfItems - 1, index)
|
||||||
|
let indexPath = IndexPath(item: index, section: 0)
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: position, animated: true)
|
||||||
|
indexOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ZKCycleScrollView: UICollectionViewDataSource {
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
numberOfItems = dataSource?.numberOfItems(in: self) ?? 0
|
||||||
|
if isInfiniteLoop && numberOfItems > 1 {
|
||||||
|
numberOfItems += numberOfAddedCells()
|
||||||
|
}
|
||||||
|
return numberOfItems
|
||||||
|
}
|
||||||
|
|
||||||
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let index = changeIndex(indexPath.item)
|
||||||
|
return (dataSource?.cycleScrollView(self, cellForItemAt: index))!
|
||||||
|
}
|
||||||
|
}
|