parent
59be003fc7
commit
5381d1ffda
|
|
@ -191,6 +191,11 @@
|
||||||
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC185F2FD12A020041DCD1 /* VipWaivePopView.swift */; };
|
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC185F2FD12A020041DCD1 /* VipWaivePopView.swift */; };
|
||||||
30EFF2992FD65FB000EB35D4 /* VoicePlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF2982FD65FB000EB35D4 /* VoicePlayerManager.swift */; };
|
30EFF2992FD65FB000EB35D4 /* VoicePlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF2982FD65FB000EB35D4 /* VoicePlayerManager.swift */; };
|
||||||
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF29A2FD668C900EB35D4 /* VoiceRecordView.swift */; };
|
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF29A2FD668C900EB35D4 /* VoiceRecordView.swift */; };
|
||||||
|
30EFF3A42FD7C5A300EB35D4 /* GroupSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3A32FD7C5A300EB35D4 /* GroupSettingView.swift */; };
|
||||||
|
30EFF3A62FD7C5AF00EB35D4 /* GroupSettingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3A52FD7C5AF00EB35D4 /* GroupSettingVC.swift */; };
|
||||||
|
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3A72FD7C6A400EB35D4 /* GroupSettingViewModel.swift */; };
|
||||||
|
30EFF3AE2FD7FF1400EB35D4 /* TextInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3AC2FD7FF1400EB35D4 /* TextInputViewController.swift */; };
|
||||||
|
30EFF3B02FD8122E00EB35D4 /* GroupTagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3AF2FD8122E00EB35D4 /* GroupTagListView.swift */; };
|
||||||
C49B37352A45A02C28FF41BA /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1C77B42994F352054070537 /* Pods_QuickLocation.framework */; };
|
C49B37352A45A02C28FF41BA /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1C77B42994F352054070537 /* Pods_QuickLocation.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
|
@ -387,6 +392,13 @@
|
||||||
30DC185F2FD12A020041DCD1 /* VipWaivePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipWaivePopView.swift; sourceTree = "<group>"; };
|
30DC185F2FD12A020041DCD1 /* VipWaivePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipWaivePopView.swift; sourceTree = "<group>"; };
|
||||||
30EFF2982FD65FB000EB35D4 /* VoicePlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoicePlayerManager.swift; sourceTree = "<group>"; };
|
30EFF2982FD65FB000EB35D4 /* VoicePlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoicePlayerManager.swift; sourceTree = "<group>"; };
|
||||||
30EFF29A2FD668C900EB35D4 /* VoiceRecordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceRecordView.swift; sourceTree = "<group>"; };
|
30EFF29A2FD668C900EB35D4 /* VoiceRecordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceRecordView.swift; sourceTree = "<group>"; };
|
||||||
|
30EFF3A02FD7A47900EB35D4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.storyboard"; sourceTree = "<group>"; };
|
||||||
|
30EFF3A12FD7A47900EB35D4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = "zh-Hans"; path = "zh-Hans.lproj/Main.storyboard"; sourceTree = "<group>"; };
|
||||||
|
30EFF3A32FD7C5A300EB35D4 /* GroupSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSettingView.swift; sourceTree = "<group>"; };
|
||||||
|
30EFF3A52FD7C5AF00EB35D4 /* GroupSettingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSettingVC.swift; sourceTree = "<group>"; };
|
||||||
|
30EFF3A72FD7C6A400EB35D4 /* GroupSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSettingViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
30EFF3AC2FD7FF1400EB35D4 /* TextInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputViewController.swift; sourceTree = "<group>"; };
|
||||||
|
30EFF3AF2FD8122E00EB35D4 /* GroupTagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTagListView.swift; sourceTree = "<group>"; };
|
||||||
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
93647DF3683AA5E71EC2FB1A /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; };
|
93647DF3683AA5E71EC2FB1A /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
D1C77B42994F352054070537 /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D1C77B42994F352054070537 /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
|
@ -396,8 +408,6 @@
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
3070777D2FD2A214004C37CC /* lotties */ = {
|
3070777D2FD2A214004C37CC /* lotties */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = lotties;
|
path = lotties;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
|
@ -759,6 +769,7 @@
|
||||||
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
||||||
305A76232FCA8C7000227D26 /* GroupView.swift */,
|
305A76232FCA8C7000227D26 /* GroupView.swift */,
|
||||||
307073E42FD18A20004C37CC /* GroupChat */,
|
307073E42FD18A20004C37CC /* GroupChat */,
|
||||||
|
30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
|
||||||
3062E8B82FCEAC5600CEF511 /* CreateGroup */,
|
3062E8B82FCEAC5600CEF511 /* CreateGroup */,
|
||||||
30BAB8612FCD714700C33B5C /* Join */,
|
30BAB8612FCD714700C33B5C /* Join */,
|
||||||
30BAB84B2FCD2FA400C33B5C /* InviteJoin */,
|
30BAB84B2FCD2FA400C33B5C /* InviteJoin */,
|
||||||
|
|
@ -830,6 +841,7 @@
|
||||||
3062E8C52FCFD01000CEF511 /* VipRecharge */,
|
3062E8C52FCFD01000CEF511 /* VipRecharge */,
|
||||||
3062E8B32FCE6BA400CEF511 /* Scan */,
|
3062E8B32FCE6BA400CEF511 /* Scan */,
|
||||||
30DC18592FD11E7A0041DCD1 /* Web */,
|
30DC18592FD11E7A0041DCD1 /* Web */,
|
||||||
|
30EFF3AD2FD7FF1400EB35D4 /* TextInput */,
|
||||||
);
|
);
|
||||||
path = Section;
|
path = Section;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -1079,6 +1091,25 @@
|
||||||
path = Web;
|
path = Web;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
30EFF3A22FD7C58400EB35D4 /* GroupSetting */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
30EFF3A52FD7C5AF00EB35D4 /* GroupSettingVC.swift */,
|
||||||
|
30EFF3A32FD7C5A300EB35D4 /* GroupSettingView.swift */,
|
||||||
|
30EFF3A72FD7C6A400EB35D4 /* GroupSettingViewModel.swift */,
|
||||||
|
30EFF3AF2FD8122E00EB35D4 /* GroupTagListView.swift */,
|
||||||
|
);
|
||||||
|
path = GroupSetting;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
30EFF3AD2FD7FF1400EB35D4 /* TextInput */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
30EFF3AC2FD7FF1400EB35D4 /* TextInputViewController.swift */,
|
||||||
|
);
|
||||||
|
path = TextInput;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3E4358FF2FC48D26003470A5 = {
|
3E4358FF2FC48D26003470A5 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -1148,11 +1179,11 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 3E4359032FC48D26003470A5 /* Build configuration list for PBXProject "QuickLocation" */;
|
buildConfigurationList = 3E4359032FC48D26003470A5 /* Build configuration list for PBXProject "QuickLocation" */;
|
||||||
developmentRegion = en;
|
developmentRegion = "zh-Hans";
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
|
||||||
Base,
|
Base,
|
||||||
|
"zh-Hans",
|
||||||
);
|
);
|
||||||
mainGroup = 3E4358FF2FC48D26003470A5;
|
mainGroup = 3E4358FF2FC48D26003470A5;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
|
@ -1216,10 +1247,14 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks.sh\"\n";
|
||||||
|
|
@ -1233,10 +1268,14 @@
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources.sh\"\n";
|
||||||
|
|
@ -1253,6 +1292,7 @@
|
||||||
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */,
|
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */,
|
||||||
305A768A2FCA8C7000227D26 /* Single+Response.swift in Sources */,
|
305A768A2FCA8C7000227D26 /* Single+Response.swift in Sources */,
|
||||||
305A768B2FCA8C7000227D26 /* API.swift in Sources */,
|
305A768B2FCA8C7000227D26 /* API.swift in Sources */,
|
||||||
|
30EFF3AE2FD7FF1400EB35D4 /* TextInputViewController.swift in Sources */,
|
||||||
305A768C2FCA8C7000227D26 /* APIProvider.swift in Sources */,
|
305A768C2FCA8C7000227D26 /* APIProvider.swift in Sources */,
|
||||||
3062E8C92FCFD03B00CEF511 /* VipRechargeVC.swift in Sources */,
|
3062E8C92FCFD03B00CEF511 /* VipRechargeVC.swift in Sources */,
|
||||||
305A768D2FCA8C7000227D26 /* AppNetworkConfig.swift in Sources */,
|
305A768D2FCA8C7000227D26 /* AppNetworkConfig.swift in Sources */,
|
||||||
|
|
@ -1279,6 +1319,7 @@
|
||||||
305A769F2FCA8C7000227D26 /* TextContentArrowCell.swift in Sources */,
|
305A769F2FCA8C7000227D26 /* TextContentArrowCell.swift in Sources */,
|
||||||
305A76A02FCA8C7000227D26 /* TextTableViewCell.swift in Sources */,
|
305A76A02FCA8C7000227D26 /* TextTableViewCell.swift in Sources */,
|
||||||
305A76A12FCA8C7000227D26 /* UIButton+RTL.m in Sources */,
|
305A76A12FCA8C7000227D26 /* UIButton+RTL.m in Sources */,
|
||||||
|
30EFF3A62FD7C5AF00EB35D4 /* GroupSettingVC.swift in Sources */,
|
||||||
305A76A22FCA8C7000227D26 /* Array+Extension.swift in Sources */,
|
305A76A22FCA8C7000227D26 /* Array+Extension.swift in Sources */,
|
||||||
305A76A32FCA8C7000227D26 /* ControlEvents+Block.swift in Sources */,
|
305A76A32FCA8C7000227D26 /* ControlEvents+Block.swift in Sources */,
|
||||||
3062E8B72FCE6BFE00CEF511 /* ScanView.swift in Sources */,
|
3062E8B72FCE6BFE00CEF511 /* ScanView.swift in Sources */,
|
||||||
|
|
@ -1303,6 +1344,7 @@
|
||||||
305A76B42FCA8C7000227D26 /* UINavigationController+FDFullscreenPopGesture.m in Sources */,
|
305A76B42FCA8C7000227D26 /* UINavigationController+FDFullscreenPopGesture.m in Sources */,
|
||||||
305A76B52FCA8C7000227D26 /* UITableView+Extension.swift in Sources */,
|
305A76B52FCA8C7000227D26 /* UITableView+Extension.swift in Sources */,
|
||||||
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
|
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
|
||||||
|
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
|
||||||
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
|
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
|
||||||
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
|
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
|
||||||
305A76B92FCA8C7000227D26 /* URL+Extension.swift in Sources */,
|
305A76B92FCA8C7000227D26 /* URL+Extension.swift in Sources */,
|
||||||
|
|
@ -1343,6 +1385,7 @@
|
||||||
305A76D62FCA8C7000227D26 /* ImagePlugin.swift in Sources */,
|
305A76D62FCA8C7000227D26 /* ImagePlugin.swift in Sources */,
|
||||||
305A76D72FCA8C7000227D26 /* NotEmpty.swift in Sources */,
|
305A76D72FCA8C7000227D26 /* NotEmpty.swift in Sources */,
|
||||||
305A76D82FCA8C7000227D26 /* Action.swift in Sources */,
|
305A76D82FCA8C7000227D26 /* Action.swift in Sources */,
|
||||||
|
30EFF3B02FD8122E00EB35D4 /* GroupTagListView.swift in Sources */,
|
||||||
305A76D92FCA8C7000227D26 /* Action+Internal.swift in Sources */,
|
305A76D92FCA8C7000227D26 /* Action+Internal.swift in Sources */,
|
||||||
305A76DA2FCA8C7000227D26 /* Button+Action.swift in Sources */,
|
305A76DA2FCA8C7000227D26 /* Button+Action.swift in Sources */,
|
||||||
305A76DB2FCA8C7000227D26 /* Control+Action.swift in Sources */,
|
305A76DB2FCA8C7000227D26 /* Control+Action.swift in Sources */,
|
||||||
|
|
@ -1384,6 +1427,7 @@
|
||||||
305A76F32FCA8C7000227D26 /* AutoLayout+NSLayoutConstraint.swift in Sources */,
|
305A76F32FCA8C7000227D26 /* AutoLayout+NSLayoutConstraint.swift in Sources */,
|
||||||
305A76F42FCA8C7000227D26 /* AutoLayout+UIView.swift in Sources */,
|
305A76F42FCA8C7000227D26 /* AutoLayout+UIView.swift in Sources */,
|
||||||
305A76F52FCA8C7000227D26 /* AutoLayoutSwift.swift in Sources */,
|
305A76F52FCA8C7000227D26 /* AutoLayoutSwift.swift in Sources */,
|
||||||
|
30EFF3A42FD7C5A300EB35D4 /* GroupSettingView.swift in Sources */,
|
||||||
305A76F62FCA8C7000227D26 /* AppRouter.swift in Sources */,
|
305A76F62FCA8C7000227D26 /* AppRouter.swift in Sources */,
|
||||||
305A76F72FCA8C7000227D26 /* RouterTarget.swift in Sources */,
|
305A76F72FCA8C7000227D26 /* RouterTarget.swift in Sources */,
|
||||||
307073E52FD18A20004C37CC /* GroupChatView.swift in Sources */,
|
307073E52FD18A20004C37CC /* GroupChatView.swift in Sources */,
|
||||||
|
|
@ -1434,6 +1478,7 @@
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
305A76812FCA8C7000227D26 /* Base */,
|
305A76812FCA8C7000227D26 /* Base */,
|
||||||
|
30EFF3A02FD7A47900EB35D4 /* zh-Hans */,
|
||||||
);
|
);
|
||||||
name = LaunchScreen.storyboard;
|
name = LaunchScreen.storyboard;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -1442,6 +1487,7 @@
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
305A76832FCA8C7000227D26 /* Base */,
|
305A76832FCA8C7000227D26 /* Base */,
|
||||||
|
30EFF3A12FD7A47900EB35D4 /* zh-Hans */,
|
||||||
);
|
);
|
||||||
name = Main.storyboard;
|
name = Main.storyboard;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -1463,6 +1509,12 @@
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = QuickLocation/Info.plist;
|
INFOPLIST_FILE = QuickLocation/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "极速定位";
|
INFOPLIST_KEY_CFBundleDisplayName = "极速定位";
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "您的相机将被用于扫描二维码、拍摄照片和视频。";
|
||||||
|
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "需要获取您的位置信息以在地图上显示您的位置";
|
||||||
|
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要获取您的位置信息以在地图上显示您的位置";
|
||||||
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "我们需要使用您的麦克风,以便您使用麦克风进行音频录制。";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "为了保存图片到您的相册,请允许添加照片,谢谢。";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的相册,以便您可以发送照片。";
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
|
|
@ -1505,6 +1557,12 @@
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = QuickLocation/Info.plist;
|
INFOPLIST_FILE = QuickLocation/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "极速定位";
|
INFOPLIST_KEY_CFBundleDisplayName = "极速定位";
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "您的相机将被用于扫描二维码、拍摄照片和视频。";
|
||||||
|
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "需要获取您的位置信息以在地图上显示您的位置";
|
||||||
|
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "需要获取您的位置信息以在地图上显示您的位置";
|
||||||
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "我们需要使用您的麦克风,以便您使用麦克风进行音频录制。";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "为了保存图片到您的相册,请允许添加照片,谢谢。";
|
||||||
|
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的相册,以便您可以发送照片。";
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
|
|
@ -1538,6 +1596,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
|
@ -1601,6 +1660,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -7,14 +7,6 @@
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>您的相机将被用于扫描二维码、拍摄照片和视频。</string>
|
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
|
||||||
<string>需要获取您的位置信息以在地图上显示您的位置</string>
|
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
|
||||||
<string>需要获取您的位置信息以在地图上显示您的位置</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>我们需要使用您的麦克风,以便您使用麦克风进行音频录制。</string>
|
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>douyu.otf</string>
|
<string>douyu.otf</string>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ enum Route: String {
|
||||||
case vipRights = "vipRights"
|
case vipRights = "vipRights"
|
||||||
/// 群聊
|
/// 群聊
|
||||||
case groupChat = "groupChat"
|
case groupChat = "groupChat"
|
||||||
|
/// 圈子设置
|
||||||
|
case groupSetting = "groupSetting"
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Route: RouterTarget {
|
extension Route: RouterTarget {
|
||||||
|
|
@ -149,10 +151,17 @@ extension AppRouter: AppRouterProtocol {
|
||||||
VipRightsVC()
|
VipRightsVC()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 群聊
|
||||||
AppRouter.register(Route.groupChat) { url, parameters in
|
AppRouter.register(Route.groupChat) { url, parameters in
|
||||||
let groupId = parameters["groupId"].safeString
|
let groupId = parameters["groupId"].safeString
|
||||||
return GroupChatVC(groupId: groupId)
|
return GroupChatVC(groupId: groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 圈子设置
|
||||||
|
AppRouter.register(Route.groupSetting) { url, parameters in
|
||||||
|
let groupId = parameters["groupId"].safeString
|
||||||
|
return GroupSettingVC(groupId: groupId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ public struct ThemeColor: Mappable {
|
||||||
/// 用于次要信息、辅助功能,如提示说明文字 搜索栏默认文字 #999999
|
/// 用于次要信息、辅助功能,如提示说明文字 搜索栏默认文字 #999999
|
||||||
public var contentColor = UIColor(hexStr: "#999999")
|
public var contentColor = UIColor(hexStr: "#999999")
|
||||||
/// 线条
|
/// 线条
|
||||||
public var lineColor = UIColor(hexStr: "#F2F2F2")
|
public var lineColor = UIColor(hexStr: "#EEEEEE")
|
||||||
/// 背景色
|
/// 背景色
|
||||||
public var backgroundColor = UIColor(hexStr: "#F2F2F2")
|
public var backgroundColor = UIColor(hexStr: "#F2F2F2")
|
||||||
/// 商品卡片背景色
|
/// 商品卡片背景色
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,13 @@ struct GroupInfoModel: Mappable, Equatable {
|
||||||
var groupIcon: UIImage {
|
var groupIcon: UIImage {
|
||||||
UIImage(named: "GroupIcon/\(icon_index)") ?? UIImage()
|
UIImage(named: "GroupIcon/\(icon_index)") ?? UIImage()
|
||||||
}
|
}
|
||||||
|
/// 标签
|
||||||
|
var labels: [String] = []
|
||||||
|
/// 审核开关
|
||||||
|
var review: Bool = false
|
||||||
/// 人数
|
/// 人数
|
||||||
var people_no: Int = 0
|
var people_no: Int = 0
|
||||||
///
|
/// 描述
|
||||||
var description: String = ""
|
var description: String = ""
|
||||||
/// 会员等级
|
/// 会员等级
|
||||||
var level: String = ""
|
var level: String = ""
|
||||||
|
|
@ -121,7 +125,10 @@ struct GroupInfoModel: Mappable, Equatable {
|
||||||
is_owner <- map["is_owner"]
|
is_owner <- map["is_owner"]
|
||||||
name <- map["name"]
|
name <- map["name"]
|
||||||
icon_index <- map["icon_index"]
|
icon_index <- map["icon_index"]
|
||||||
|
labels <- map["labels"]
|
||||||
level <- map["level"]
|
level <- map["level"]
|
||||||
|
review <- map["review"]
|
||||||
|
description <- map["description"]
|
||||||
people_no <- (map["people_no"], kStrTransformInt)
|
people_no <- (map["people_no"], kStrTransformInt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,311 @@
|
||||||
|
//
|
||||||
|
// TextInputViewController.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
// Created by 八条 on 2026/6/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
|
||||||
|
/// 通用文本输入页面
|
||||||
|
/// 用法:
|
||||||
|
/// let vc = TextInputViewController(title: "编辑昵称", maxLength: 20) { text in
|
||||||
|
/// print("用户输入: \(text)")
|
||||||
|
/// }
|
||||||
|
/// present(vc, animated: true)
|
||||||
|
final class TextInputViewController: UIViewController {
|
||||||
|
|
||||||
|
private let titleText: String
|
||||||
|
private let maxLength: Int
|
||||||
|
private let confirmAction: ((String) -> Void)?
|
||||||
|
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
private let textRelay = BehaviorRelay<String>(value: "")
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
|
||||||
|
/// - Parameters:
|
||||||
|
/// - title: 页面标题
|
||||||
|
/// - maxLength: 文字输入上限(0 表示不限制)
|
||||||
|
/// - initialText: 初始文本,默认空
|
||||||
|
/// - confirmAction: 确定回调
|
||||||
|
init(title: String,
|
||||||
|
maxLength: Int = 0,
|
||||||
|
initialText: String = "",
|
||||||
|
confirmAction: ((String) -> Void)? = nil) {
|
||||||
|
self.titleText = title
|
||||||
|
self.maxLength = maxLength
|
||||||
|
self.confirmAction = confirmAction
|
||||||
|
self.textRelay.accept(initialText)
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
modalPresentationStyle = .fullScreen
|
||||||
|
modalTransitionStyle = .coverVertical
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#F5FBFB")
|
||||||
|
setupUI()
|
||||||
|
setupBinding()
|
||||||
|
setupKeyboard()
|
||||||
|
textView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
if textViewHeightConstraint == nil {
|
||||||
|
updateTextViewHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UI
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
view.addSubview(topBar)
|
||||||
|
topBar.addSubview(closeBtn)
|
||||||
|
topBar.addSubview(titleLabel)
|
||||||
|
|
||||||
|
view.addSubview(contentView)
|
||||||
|
contentView.addSubview(textView)
|
||||||
|
textView.addSubview(countLabel)
|
||||||
|
contentView.addSubview(confirmBtn)
|
||||||
|
|
||||||
|
// 顶栏
|
||||||
|
topBar.layoutChain
|
||||||
|
.top()
|
||||||
|
.edgesHorzontal()
|
||||||
|
.height(kNaviHeight)
|
||||||
|
|
||||||
|
closeBtn.layoutChain
|
||||||
|
.bottom(12)
|
||||||
|
.left(7)
|
||||||
|
.width(24).height(24)
|
||||||
|
|
||||||
|
titleLabel.layoutChain
|
||||||
|
.centerY(closeBtn)
|
||||||
|
.centerX()
|
||||||
|
|
||||||
|
// 内容容器(textView + 按钮),键盘升起时整体上移
|
||||||
|
contentView.layoutChain
|
||||||
|
.topToBottomOfView(topBar, offset: 16)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
.bottom()
|
||||||
|
|
||||||
|
// 输入框
|
||||||
|
textView.layoutChain
|
||||||
|
.top()
|
||||||
|
.edgesHorzontal()
|
||||||
|
.height(textViewMinHeight)
|
||||||
|
|
||||||
|
// 字数统计(textView 右下角)
|
||||||
|
countLabel.layoutChain
|
||||||
|
.right(-10)
|
||||||
|
.bottom(-8)
|
||||||
|
|
||||||
|
// 确定按钮
|
||||||
|
confirmBtn.layoutChain
|
||||||
|
.topToBottomOfView(textView, offset: 50)
|
||||||
|
.edgesHorzontal()
|
||||||
|
.height(44)
|
||||||
|
.bottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binding
|
||||||
|
|
||||||
|
private func setupBinding() {
|
||||||
|
// 输入流
|
||||||
|
textView.rx.text
|
||||||
|
.compactMap { $0 }
|
||||||
|
.subscribe(onNext: { [weak self] text in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let realText = self.maxLength > 0 && text.count > self.maxLength
|
||||||
|
? String(text.prefix(self.maxLength))
|
||||||
|
: text
|
||||||
|
if realText != text {
|
||||||
|
self.textView.text = realText
|
||||||
|
}
|
||||||
|
self.textRelay.accept(realText)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 字数统计
|
||||||
|
textRelay
|
||||||
|
.map { [weak self] text in
|
||||||
|
guard let self = self, self.maxLength > 0 else { return "" }
|
||||||
|
return "\(text.count)/\(self.maxLength)"
|
||||||
|
}
|
||||||
|
.bind(to: countLabel.rx.text)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 确定按钮状态 + 背景色
|
||||||
|
let confirmEnabled = textRelay
|
||||||
|
.map { [weak self] text in
|
||||||
|
guard let self = self else { return false }
|
||||||
|
if self.maxLength > 0 { return !text.isEmpty && text.count <= self.maxLength }
|
||||||
|
return !text.isEmpty
|
||||||
|
}
|
||||||
|
.share(replay: 1)
|
||||||
|
|
||||||
|
confirmEnabled
|
||||||
|
.bind(to: confirmBtn.rx.isEnabled)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
confirmEnabled
|
||||||
|
.subscribe(onNext: { [weak self] enabled in
|
||||||
|
self?.confirmBtn.backgroundColor = enabled
|
||||||
|
? UIColor(hexStr: "#16B3FF")
|
||||||
|
: UIColor(hexStr: "#CCCCCC")
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 动态高度
|
||||||
|
textView.rx.text
|
||||||
|
.observe(on: MainScheduler.asyncInstance)
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
self?.updateTextViewHeight()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 确定
|
||||||
|
confirmBtn.rx.tap
|
||||||
|
.withLatestFrom(textRelay)
|
||||||
|
.subscribe(onNext: { [weak self] text in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.confirmAction?(text)
|
||||||
|
self.dismiss(animated: true)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
closeBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
self?.dismiss(animated: true)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Keyboard
|
||||||
|
|
||||||
|
private var originContentY: CGFloat = 0
|
||||||
|
|
||||||
|
private func setupKeyboard() {
|
||||||
|
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
|
||||||
|
.subscribe(onNext: { [weak self] noti in
|
||||||
|
guard let self = self,
|
||||||
|
let userInfo = noti.userInfo,
|
||||||
|
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
|
||||||
|
else { return }
|
||||||
|
let keyboardHeight = frame.height
|
||||||
|
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
||||||
|
// 计算 contentView 需要上移的距离 = 键盘遮住底部的高度 - 安全区域
|
||||||
|
let offset = keyboardHeight - kSafeBottomMargin
|
||||||
|
UIView.animate(withDuration: duration) {
|
||||||
|
self.contentView.transform = CGAffineTransform(translationX: 0, y: -offset)
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification)
|
||||||
|
.subscribe(onNext: { [weak self] noti in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let duration = (noti.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
||||||
|
UIView.animate(withDuration: duration) {
|
||||||
|
self.contentView.transform = .identity
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TextView Height
|
||||||
|
|
||||||
|
private let textViewMinHeight: CGFloat = 150
|
||||||
|
private let textViewMaxHeight: CGFloat = 300
|
||||||
|
private var textViewHeightConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
private func updateTextViewHeight() {
|
||||||
|
let size = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: CGFloat.greatestFiniteMagnitude))
|
||||||
|
let height = min(max(size.height, textViewMinHeight), textViewMaxHeight)
|
||||||
|
if textViewHeightConstraint == nil {
|
||||||
|
textViewHeightConstraint = textView.layoutChain.height(height)
|
||||||
|
} else {
|
||||||
|
textViewHeightConstraint?.constant = height
|
||||||
|
}
|
||||||
|
UIView.setAnimationsEnabled(false)
|
||||||
|
textView.layoutIfNeeded()
|
||||||
|
UIView.setAnimationsEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
|
||||||
|
private lazy var contentView: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .clear
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var topBar: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .clear
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var closeBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setImage(UIImage(named: "Common/back"), for: .normal)
|
||||||
|
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 30)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 17, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#333333")
|
||||||
|
label.text = titleText
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var textView: UITextView = {
|
||||||
|
let tv = UITextView()
|
||||||
|
tv.font = .systemFont(ofSize: 15)
|
||||||
|
tv.textColor = UIColor(hexStr: "#333333")
|
||||||
|
tv.backgroundColor = .white
|
||||||
|
tv.cornerRadius = 4
|
||||||
|
tv.layer.borderWidth = 1
|
||||||
|
tv.layer.borderColor = ThemeManager.shared.color.lineColor.cgColor
|
||||||
|
tv.textContainerInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
||||||
|
tv.showsVerticalScrollIndicator = true
|
||||||
|
tv.bounces = false
|
||||||
|
tv.tintColor = UIColor(hexStr: "#16B3FF")
|
||||||
|
return tv
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var countLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 12)
|
||||||
|
label.textColor = UIColor(hexStr: "#BBBBBB")
|
||||||
|
label.text = maxLength > 0 ? "0/\(maxLength)" : ""
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var confirmBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setTitle("确定", for: .normal)
|
||||||
|
btn.setTitleColor(.white, for: .normal)
|
||||||
|
btn.setTitleColor(.white, for: .disabled)
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||||
|
btn.cornerRadius = 22
|
||||||
|
btn.isEnabled = false
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,5 @@ class GroupIconListVC: BaseViewController {
|
||||||
extension GroupIconListVC: UICollectionViewDelegate {
|
extension GroupIconListVC: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
rootView.selectedIndex = indexPath.row + 1
|
rootView.selectedIndex = indexPath.row + 1
|
||||||
onSelectIcon?(rootView.selectedIndex)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import RxDataSources
|
||||||
import OpenIMSDK
|
import OpenIMSDK
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
|
import HXPHPicker
|
||||||
|
import IQKeyboardManagerSwift
|
||||||
|
|
||||||
final class GroupChatVC: BaseViewController {
|
final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
|
|
@ -45,6 +47,49 @@ final class GroupChatVC: BaseViewController {
|
||||||
setupMessageListener()
|
setupMessageListener()
|
||||||
setupVoiceRecording()
|
setupVoiceRecording()
|
||||||
setupPanelDismiss()
|
setupPanelDismiss()
|
||||||
|
setupKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
IQKeyboardManager.shared.isEnabled = false
|
||||||
|
IQKeyboardManager.shared.resignOnTouchOutside = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
// if !hasScrolledToBottom {
|
||||||
|
// hasScrolledToBottom = true
|
||||||
|
// scrollToBottom()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
VoicePlayerManager.shared.stop()
|
||||||
|
IQKeyboardManager.shared.isEnabled = true
|
||||||
|
IQKeyboardManager.shared.resignOnTouchOutside = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Keyboard
|
||||||
|
private func setupKeyboard() {
|
||||||
|
// 键盘升起
|
||||||
|
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
|
||||||
|
.subscribe(onNext: { [weak self] noti in
|
||||||
|
guard let self = self,
|
||||||
|
let userInfo = noti.userInfo,
|
||||||
|
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
|
||||||
|
else { return }
|
||||||
|
let height = frame.height
|
||||||
|
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
||||||
|
// 收起表情/语音面板
|
||||||
|
self.rootView.dismissAllPanels(excludeTextField: true)
|
||||||
|
UIView.animate(withDuration: duration) {
|
||||||
|
self.rootView.bottomBar.layoutChain.bottom(height + kSafeBottomMargin + 20)
|
||||||
|
}
|
||||||
|
self.scrollToBottom()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupPanelDismiss() {
|
private func setupPanelDismiss() {
|
||||||
|
|
@ -89,19 +134,6 @@ final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
private var hasScrolledToBottom = false
|
private var hasScrolledToBottom = false
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
if !hasScrolledToBottom {
|
|
||||||
hasScrolledToBottom = true
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
VoicePlayerManager.shared.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scrollToBottom() {
|
private func scrollToBottom() {
|
||||||
let count = dataSource.sectionModels.first?.items.count ?? 0
|
let count = dataSource.sectionModels.first?.items.count ?? 0
|
||||||
guard count > 0 else { return }
|
guard count > 0 else { return }
|
||||||
|
|
@ -112,50 +144,6 @@ final class GroupChatVC: BaseViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - dataSource
|
|
||||||
private lazy var dataSource: RxTableViewSectionedReloadDataSource<ChatSectionModel> = {
|
|
||||||
RxTableViewSectionedReloadDataSource<ChatSectionModel> { _, tableView, indexPath, item in
|
|
||||||
switch item {
|
|
||||||
case let .send(msg):
|
|
||||||
let cell: TextSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .received(msg):
|
|
||||||
let cell: TextReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .emojiSend(msg):
|
|
||||||
let cell: EmojiSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .emojiReceived(msg):
|
|
||||||
let cell: EmojiReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .voiceSend(msg):
|
|
||||||
let cell: VoiceSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .voiceReceived(msg):
|
|
||||||
let cell: VoiceReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .imageSend(msg):
|
|
||||||
let cell: ImageSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .imageReceived(msg):
|
|
||||||
let cell: ImageReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(msg)
|
|
||||||
return cell
|
|
||||||
case let .notification(text):
|
|
||||||
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
|
||||||
cell.configure(text)
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var emojiDataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
|
private lazy var emojiDataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
|
||||||
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
||||||
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||||
|
|
@ -183,25 +171,58 @@ final class GroupChatVC: BaseViewController {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.emojiBtn.rx.tap
|
// 语音按钮
|
||||||
.subscribe(onNext: { [weak self] _ in
|
rootView.voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let show = self.rootView.emojiPanelView.isHidden
|
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
||||||
self.rootView.emojiPanelView.isHidden = !show
|
switch status {
|
||||||
let offset: CGFloat = show ? 220 : 0
|
case .authorized:
|
||||||
UIView.animate(withDuration: 0.25) {
|
break
|
||||||
self.rootView.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20 + offset)
|
case .notDetermined:
|
||||||
} completion: { _ in
|
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
||||||
let offset: CGFloat = self.rootView.tableView.contentSize.height
|
guard granted else { return }
|
||||||
self.rootView.tableView.setContentOffset(CGPointMake(0, offset), animated: false)
|
DispatchQueue.main.async {
|
||||||
|
self.rootView.dismissAllPanels()
|
||||||
|
self.showSpeakPanel()
|
||||||
}
|
}
|
||||||
if show {
|
|
||||||
self.rootView.textField.resignFirstResponder()
|
|
||||||
EmojiPanelCell.preloadAnimations()
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
Permission.openAppSetting(title: "请开启麦克风权限",
|
||||||
|
message: "请在iPhone的“设置-隐私-麦克风”选项中允许\(kAppName)访问你的麦克风。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.rootView.dismissAllPanels()
|
||||||
|
self.showSpeakPanel()
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 键盘按钮
|
||||||
|
rootView.voiceRecordView.keyboardBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.rootView.dismissAllPanels()
|
||||||
|
self.rootView.textField.becomeFirstResponder()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 表情按钮
|
||||||
|
rootView.emojiBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.showEmojiPanel()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.voiceRecordView.emojiBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.rootView.dismissAllPanels()
|
||||||
|
self.showEmojiPanel()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 表情面板点击
|
||||||
rootView.emojiCollectionView.rx.modelSelected(String.self)
|
rootView.emojiCollectionView.rx.modelSelected(String.self)
|
||||||
.subscribe(onNext: { [weak self] name in
|
.subscribe(onNext: { [weak self] name in
|
||||||
guard let self = self, let idx = UITableViewCell.emojiFileNames.firstIndex(of: name) else { return }
|
guard let self = self, let idx = UITableViewCell.emojiFileNames.firstIndex(of: name) else { return }
|
||||||
|
|
@ -209,7 +230,17 @@ final class GroupChatVC: BaseViewController {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// ➕按钮
|
||||||
|
Observable.merge(
|
||||||
|
rootView.addBtn.rx.tap.asObservable(),
|
||||||
|
rootView.voiceRecordView.addBtn.rx.tap.asObservable()
|
||||||
|
)
|
||||||
|
.subscribe(onNext: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.rootView.dismissAllPanels()
|
||||||
|
self.showAlbum()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
let sendText = Observable.merge(
|
let sendText = Observable.merge(
|
||||||
rootView.sendBtn.rx.tap.map { [weak self] _ in self?.rootView.textField.text ?? "" },
|
rootView.sendBtn.rx.tap.map { [weak self] _ in self?.rootView.textField.text ?? "" },
|
||||||
|
|
@ -222,6 +253,65 @@ final class GroupChatVC: BaseViewController {
|
||||||
sendText
|
sendText
|
||||||
.bind(to: viewModel.input.sendMessage)
|
.bind(to: viewModel.input.sendMessage)
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 设置
|
||||||
|
rootView.settingBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
AppRouter.push(Route.groupSetting, userInfo: ["groupId": self.viewModel.groupId])
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 显示语音面板
|
||||||
|
private func showSpeakPanel() {
|
||||||
|
let show = rootView.voiceRecordView.isHidden
|
||||||
|
rootView.voiceRecordView.isHidden = !show
|
||||||
|
let offset: CGFloat = show ? 252 : 0
|
||||||
|
UIView.animate(withDuration: 0.25) {
|
||||||
|
self.rootView.bottomBar.layoutChain.bottom(show ? offset - self.rootView.bottomBar.dl.height : kSafeBottomMargin + 20)
|
||||||
|
self.rootView.voiceRecordView.layoutChain.bottom(offset - 252 + kSafeBottomMargin)
|
||||||
|
}
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 显示表情面板
|
||||||
|
private func showEmojiPanel() {
|
||||||
|
let show = self.rootView.emojiPanelView.isHidden
|
||||||
|
self.rootView.emojiPanelView.isHidden = !show
|
||||||
|
let offset: CGFloat = show ? 220 : 0
|
||||||
|
UIView.animate(withDuration: 0.25) {
|
||||||
|
self.rootView.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20 + offset)
|
||||||
|
}
|
||||||
|
// completion: { _ in
|
||||||
|
// let offset: CGFloat = self.rootView.tableView.contentSize.height
|
||||||
|
// self.rootView.tableView.setContentOffset(CGPointMake(0, offset), animated: false)
|
||||||
|
// }
|
||||||
|
scrollToBottom()
|
||||||
|
|
||||||
|
if show {
|
||||||
|
self.rootView.textField.resignFirstResponder()
|
||||||
|
EmojiPanelCell.preloadAnimations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 显示相册
|
||||||
|
private func showAlbum() {
|
||||||
|
// 设置与微信主题一致的配置
|
||||||
|
let config = PhotoTools.getWXPickerConfig()
|
||||||
|
// 最多可以选择的资源数,如果为0则不限制
|
||||||
|
config.selectOptions = [.photo]
|
||||||
|
config.selectMode = .multiple
|
||||||
|
config.maximumSelectedCount = 9
|
||||||
|
// config.maximumSelectedPhotoFileSize = 5242880
|
||||||
|
config.allowSyncICloudWhenSelectPhoto = false
|
||||||
|
config.previewView.bottomView.editButtonHidden = true
|
||||||
|
config.photoList.allowAddCamera = true
|
||||||
|
config.photoList.camera.allowsEditing = false
|
||||||
|
let pickerController = PhotoPickerController(picker: config)
|
||||||
|
pickerController.pickerDelegate = self
|
||||||
|
pickerController.modalPresentationStyle = .fullScreen
|
||||||
|
self.present(pickerController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Voice Recording
|
// MARK: - Voice Recording
|
||||||
|
|
@ -331,6 +421,56 @@ final class GroupChatVC: BaseViewController {
|
||||||
self.viewModel.loadMessages()
|
self.viewModel.loadMessages()
|
||||||
}.disposed(by: disposeBag)
|
}.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - dataSource
|
||||||
|
private lazy var dataSource: RxTableViewSectionedReloadDataSource<ChatSectionModel> = {
|
||||||
|
RxTableViewSectionedReloadDataSource<ChatSectionModel> { _, tableView, indexPath, item in
|
||||||
|
switch item {
|
||||||
|
case let .send(msg):
|
||||||
|
let cell: TextSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
return cell
|
||||||
|
case let .received(msg):
|
||||||
|
let cell: TextReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
return cell
|
||||||
|
case let .emojiSend(msg):
|
||||||
|
let cell: EmojiSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
return cell
|
||||||
|
case let .emojiReceived(msg):
|
||||||
|
let cell: EmojiReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
return cell
|
||||||
|
case let .voiceSend(msg):
|
||||||
|
let cell: VoiceSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
return cell
|
||||||
|
case let .voiceReceived(msg):
|
||||||
|
let cell: VoiceReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
return cell
|
||||||
|
case let .imageSend(msg):
|
||||||
|
let cell: ImageSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
cell.onImageTap = { [weak self] in
|
||||||
|
self?.showBigImage(imgUrlList: [msg.imageUrl], currentPage: 0, projectiveView: cell.photoView)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
case let .imageReceived(msg):
|
||||||
|
let cell: ImageReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(msg)
|
||||||
|
cell.onImageTap = { [weak self] in
|
||||||
|
self?.showBigImage(imgUrlList: [msg.imageUrl], currentPage: 0, projectiveView: cell.photoView)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
case let .notification(text):
|
||||||
|
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(text)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MessageListenerProxy
|
// MARK: - MessageListenerProxy
|
||||||
|
|
@ -345,3 +485,97 @@ private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
|
||||||
handler(msg)
|
handler(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - PhotoPickerControllerDelegate
|
||||||
|
extension GroupChatVC: PhotoPickerControllerDelegate {
|
||||||
|
/// 选择完成之后调用
|
||||||
|
/// - Parameters:
|
||||||
|
/// - pickerController: 对应的 PhotoPickerController
|
||||||
|
/// - result: 选择的结果
|
||||||
|
/// result.photoAssets 选择的资源数组
|
||||||
|
/// result.isOriginal 是否选中原图
|
||||||
|
func pickerController(_ pickerController: PhotoPickerController,
|
||||||
|
didFinishSelection result: PickerResult) {
|
||||||
|
result.getImage { (image, photoAsset, index) in
|
||||||
|
} completionHandler: { [weak self] (images) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
for img in images {
|
||||||
|
self.sendImageMessage(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendImageMessage(_ image: UIImage) {
|
||||||
|
guard let data = image.jpegData(compressionQuality: 0.8) else { return }
|
||||||
|
let dir = NSTemporaryDirectory()
|
||||||
|
let filename = "img_\(Int(Date().timeIntervalSince1970)).jpg"
|
||||||
|
let fileURL = URL(fileURLWithPath: dir + filename)
|
||||||
|
try? data.write(to: fileURL)
|
||||||
|
|
||||||
|
let displaySize = Self.imageDisplaySize(w: image.size.width, h: image.size.height)
|
||||||
|
|
||||||
|
let msg = OIMMessageInfo.createImageMessage(fromFullPath: fileURL.path)
|
||||||
|
// 使用 SDK 的 clientMsgID 作为本地消息 ID,方便后续与监听器去重
|
||||||
|
let localId = msg.clientMsgID ?? UUID().uuidString
|
||||||
|
|
||||||
|
// 立即显示本地图片(带 loading)
|
||||||
|
let localMsg = ChatMessage(
|
||||||
|
id: localId,
|
||||||
|
isSelf: true,
|
||||||
|
avatar: viewModel.getUserAvatar(id: AppContextManager.shared.userId),
|
||||||
|
senderName: AppContextManager.shared.name,
|
||||||
|
content: "",
|
||||||
|
voiceUrl: "",
|
||||||
|
imageUrl: fileURL.path,
|
||||||
|
imageWidth: displaySize.width,
|
||||||
|
imageHeight: displaySize.height,
|
||||||
|
timestamp: Date().timeIntervalSince1970,
|
||||||
|
showTime: false,
|
||||||
|
isUploading: true
|
||||||
|
)
|
||||||
|
viewModel.appendLocalMessage(.imageSend(localMsg))
|
||||||
|
OIMManager.manager.sendMessage(msg,
|
||||||
|
recvID: "",
|
||||||
|
groupID: viewModel.groupId,
|
||||||
|
offlinePushInfo: nil,
|
||||||
|
onSuccess: { [weak self] returnedMsg in
|
||||||
|
// 服务端返回后,更新本地消息为服务端图片URL,去掉loading
|
||||||
|
// 注意:returnedMsg 是 SDK 回填服务端数据后的新 OIMMessageInfo,包含完整 URL
|
||||||
|
let networkUrl = returnedMsg?.pictureElem?.sourcePicture?.url
|
||||||
|
?? returnedMsg?.pictureElem?.bigPicture?.url
|
||||||
|
?? returnedMsg?.pictureElem?.sourcePath
|
||||||
|
?? ""
|
||||||
|
self?.viewModel.updateLocalMessage(id: localId) { chatMsg in
|
||||||
|
// 仅在服务端有URL时才替换,否则保留本地路径让图片仍可见
|
||||||
|
if !networkUrl.isEmpty {
|
||||||
|
chatMsg.imageUrl = networkUrl
|
||||||
|
}
|
||||||
|
chatMsg.isUploading = false
|
||||||
|
}
|
||||||
|
try? FileManager.default.removeItem(at: fileURL)
|
||||||
|
},
|
||||||
|
onProgress: nil as OIMNumberCallback?,
|
||||||
|
onFailure: { [weak self] code, errMsg in
|
||||||
|
print("Image send failed: \(code) \(errMsg ?? "")")
|
||||||
|
// 发送失败,隐藏 loading
|
||||||
|
self?.viewModel.updateLocalMessage(id: localId) { chatMsg in
|
||||||
|
chatMsg.isUploading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func imageDisplaySize(w: CGFloat, h: CGFloat) -> CGSize {
|
||||||
|
guard w > 0, h > 0 else { return CGSize(width: 160, height: 160) }
|
||||||
|
let maxW: CGFloat = 200, maxH: CGFloat = 250, minW: CGFloat = 80
|
||||||
|
var dw = maxW, dh = dw * (h / w)
|
||||||
|
if dh > maxH { dh = maxH; dw = dh * (w / h) }
|
||||||
|
if dw < minW { dw = minW; dh = dw * (h / w) }
|
||||||
|
return CGSize(width: dw, height: dh)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 点击取消时调用
|
||||||
|
/// - Parameter pickerController: 对应的 PhotoPickerController
|
||||||
|
func pickerController(didCancel pickerController: PhotoPickerController) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,12 @@ struct ChatMessage {
|
||||||
let senderName: String
|
let senderName: String
|
||||||
let content: String
|
let content: String
|
||||||
let voiceUrl: String
|
let voiceUrl: String
|
||||||
let imageUrl: String
|
var imageUrl: String
|
||||||
let imageWidth: CGFloat
|
let imageWidth: CGFloat
|
||||||
let imageHeight: CGFloat
|
let imageHeight: CGFloat
|
||||||
let timestamp: TimeInterval
|
let timestamp: TimeInterval
|
||||||
var showTime: Bool = false
|
var showTime: Bool = false
|
||||||
|
var isUploading: Bool = false
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupChatView: UIView {
|
class GroupChatView: UIView {
|
||||||
|
|
@ -44,54 +45,22 @@ class GroupChatView: UIView {
|
||||||
var onVoiceRecordState: ((VoiceRecordState) -> Void)?
|
var onVoiceRecordState: ((VoiceRecordState) -> Void)?
|
||||||
|
|
||||||
private func setupRx() {
|
private func setupRx() {
|
||||||
voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
|
||||||
switch status {
|
|
||||||
case .authorized:
|
|
||||||
break
|
|
||||||
case .notDetermined:
|
|
||||||
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
||||||
guard granted else { return }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.toggleVoicePanel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
Permission.openAppSetting(title: "请开启麦克风权限",
|
|
||||||
message: "请在iPhone的“设置-隐私-麦克风”选项中允许\(kAppName)访问你的麦克风。")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.toggleVoicePanel()
|
|
||||||
})
|
|
||||||
.disposed(by: disposeBag)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggleVoicePanel() {
|
|
||||||
let show = voiceRecordView.isHidden
|
|
||||||
voiceRecordView.isHidden = !show
|
|
||||||
let offset: CGFloat = show ? 252 : 0
|
|
||||||
UIView.animate(withDuration: 0.25) {
|
|
||||||
self.bottomBar.layoutChain.bottom(show ? offset - self.bottomBar.dl.height : kSafeBottomMargin + 20)
|
|
||||||
self.voiceRecordView.layoutChain.bottom(offset - 252 + kSafeBottomMargin)
|
|
||||||
} completion: { _ in
|
|
||||||
let offset: CGFloat = self.tableView.contentSize.height
|
|
||||||
self.tableView.setContentOffset(CGPointMake(0, offset), animated: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 收起所有浮层面板
|
/// 收起所有浮层面板
|
||||||
func dismissAllPanels() {
|
func dismissAllPanels(excludeTextField: Bool = false) {
|
||||||
let needsReset = !emojiPanelView.isHidden || !voiceRecordView.isHidden || !voiceRecordView.isHidden
|
let needsReset = !emojiPanelView.isHidden
|
||||||
|
|| !voiceRecordView.isHidden
|
||||||
|
|| textField.isFirstResponder
|
||||||
|
|
||||||
guard needsReset else { return }
|
guard needsReset else { return }
|
||||||
emojiPanelView.isHidden = true
|
emojiPanelView.isHidden = true
|
||||||
voiceRecordView.isHidden = true
|
voiceRecordView.isHidden = true
|
||||||
textField.resignFirstResponder()
|
if !excludeTextField { textField.resignFirstResponder() }
|
||||||
UIView.animate(withDuration: 0.25) {
|
UIView.animate(withDuration: 0.25) {
|
||||||
self.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20)
|
self.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20)
|
||||||
self.voiceRecordView.layoutChain.bottom(-252)
|
self.voiceRecordView.layoutChain.bottom(-252)
|
||||||
self.layoutIfNeeded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1226,15 +1195,33 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
// MARK: - 发送的图片消息
|
// MARK: - 发送的图片消息
|
||||||
final class ImageSendMsgCell: UITableViewCell {
|
final class ImageSendMsgCell: UITableViewCell {
|
||||||
|
|
||||||
|
var onImageTap: (() -> Void)?
|
||||||
|
|
||||||
func configure(_ msg: ChatMessage) {
|
func configure(_ msg: ChatMessage) {
|
||||||
timeLabel.isHidden = !msg.showTime
|
timeLabel.isHidden = !msg.showTime
|
||||||
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||||
avatarView.image = msg.avatar
|
avatarView.image = msg.avatar
|
||||||
if !msg.imageUrl.isEmpty {
|
if !msg.imageUrl.isEmpty {
|
||||||
|
// 优先加载本地文件路径,否则走网络加载
|
||||||
|
if let localImage = UIImage(contentsOfFile: msg.imageUrl) {
|
||||||
|
photoView.image = localImage
|
||||||
|
} else {
|
||||||
photoView.dl.setImage(with: msg.imageUrl)
|
photoView.dl.setImage(with: msg.imageUrl)
|
||||||
}
|
}
|
||||||
photoView.layoutChain.width(msg.imageWidth).height(msg.imageHeight)
|
|
||||||
}
|
}
|
||||||
|
photoView.layoutChain.width(msg.imageWidth).height(msg.imageHeight)
|
||||||
|
loadingView.isHidden = !msg.isUploading
|
||||||
|
if msg.isUploading { loadingView.startAnimating() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func onTap() { onImageTap?() }
|
||||||
|
|
||||||
|
private let loadingView: UIActivityIndicatorView = {
|
||||||
|
let v = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.large)
|
||||||
|
v.hidesWhenStopped = true
|
||||||
|
v.color = UIColor(hexStr: "#16B3FF")
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
private static func displaySize(w: CGFloat, h: CGFloat) -> CGSize {
|
private static func displaySize(w: CGFloat, h: CGFloat) -> CGSize {
|
||||||
guard w > 0, h > 0 else { return CGSize(width: 160, height: 160) }
|
guard w > 0, h > 0 else { return CGSize(width: 160, height: 160) }
|
||||||
|
|
@ -1276,12 +1263,13 @@ final class ImageSendMsgCell: UITableViewCell {
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let photoView: UIImageView = {
|
lazy var photoView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.cornerRadius = 8
|
iv.cornerRadius = 8
|
||||||
iv.clipsToBounds = true
|
iv.clipsToBounds = true
|
||||||
iv.backgroundColor = UIColor(hexStr: "#F0F0F0")
|
iv.backgroundColor = UIColor(hexStr: "#F0F0F0")
|
||||||
|
iv.isUserInteractionEnabled = true
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -1290,8 +1278,13 @@ final class ImageSendMsgCell: UITableViewCell {
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
contentView.addSubview(timeLabel)
|
contentView.addSubview(timeLabel)
|
||||||
contentView.addSubview(avatarView)
|
|
||||||
contentView.addSubview(photoView)
|
contentView.addSubview(photoView)
|
||||||
|
contentView.addSubview(avatarView)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(onTap))
|
||||||
|
photoView.addGestureRecognizer(tap)
|
||||||
|
photoView.addSubview(loadingView)
|
||||||
|
loadingView.layoutChain.centerX().centerY()
|
||||||
|
|
||||||
timeLabel.layoutChain.top().centerX()
|
timeLabel.layoutChain.top().centerX()
|
||||||
avatarView.layoutChain
|
avatarView.layoutChain
|
||||||
|
|
@ -1312,6 +1305,8 @@ final class ImageSendMsgCell: UITableViewCell {
|
||||||
// MARK: - 收到的图片消息
|
// MARK: - 收到的图片消息
|
||||||
final class ImageReceivedMsgCell: UITableViewCell {
|
final class ImageReceivedMsgCell: UITableViewCell {
|
||||||
|
|
||||||
|
var onImageTap: (() -> Void)?
|
||||||
|
|
||||||
func configure(_ msg: ChatMessage) {
|
func configure(_ msg: ChatMessage) {
|
||||||
timeLabel.isHidden = !msg.showTime
|
timeLabel.isHidden = !msg.showTime
|
||||||
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||||
|
|
@ -1323,6 +1318,8 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
||||||
photoView.layoutChain.width(msg.imageWidth).height(msg.imageHeight)
|
photoView.layoutChain.width(msg.imageWidth).height(msg.imageHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func onTap() { onImageTap?() }
|
||||||
|
|
||||||
private static func displaySize(w: CGFloat, h: CGFloat) -> CGSize {
|
private static func displaySize(w: CGFloat, h: CGFloat) -> CGSize {
|
||||||
guard w > 0, h > 0 else { return CGSize(width: 160, height: 160) }
|
guard w > 0, h > 0 else { return CGSize(width: 160, height: 160) }
|
||||||
let maxW: CGFloat = 200, maxH: CGFloat = 250, minW: CGFloat = 80
|
let maxW: CGFloat = 200, maxH: CGFloat = 250, minW: CGFloat = 80
|
||||||
|
|
@ -1370,12 +1367,13 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let photoView: UIImageView = {
|
lazy var photoView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.cornerRadius = 8
|
iv.cornerRadius = 8
|
||||||
iv.clipsToBounds = true
|
iv.clipsToBounds = true
|
||||||
iv.backgroundColor = UIColor(hexStr: "#F0F0F0")
|
iv.backgroundColor = UIColor(hexStr: "#F0F0F0")
|
||||||
|
iv.isUserInteractionEnabled = true
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -1388,6 +1386,9 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
||||||
contentView.addSubview(photoView)
|
contentView.addSubview(photoView)
|
||||||
contentView.addSubview(avatarView)
|
contentView.addSubview(avatarView)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(onTap))
|
||||||
|
photoView.addGestureRecognizer(tap)
|
||||||
|
|
||||||
timeLabel.layoutChain.top().centerX()
|
timeLabel.layoutChain.top().centerX()
|
||||||
|
|
||||||
photoView.layoutChain
|
photoView.layoutChain
|
||||||
|
|
|
||||||
|
|
@ -134,11 +134,44 @@ final class GroupChatViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 本地消息(发送中)
|
||||||
|
func appendLocalMessage(_ item: ChatSectionItem) {
|
||||||
|
var items = messagesSubject.value
|
||||||
|
items.append(item)
|
||||||
|
messagesSubject.accept(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据 id 更新本地消息(图片上传成功/失败后替换本地占位消息)
|
||||||
|
func updateLocalMessage(id: String, update: (inout ChatMessage) -> Void) {
|
||||||
|
var items = messagesSubject.value
|
||||||
|
guard let idx = items.firstIndex(where: { item in
|
||||||
|
switch item {
|
||||||
|
case let .imageSend(m): return m.id == id
|
||||||
|
case let .voiceSend(m): return m.id == id
|
||||||
|
case let .send(m): return m.id == id
|
||||||
|
case let .emojiSend(m): return m.id == id
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
var chatMsg = items[idx].chatMessage
|
||||||
|
else { return }
|
||||||
|
update(&chatMsg)
|
||||||
|
items[idx] = ChatSectionItem.with(chatMsg)
|
||||||
|
messagesSubject.accept(items)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Receive
|
// MARK: - Receive
|
||||||
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
||||||
guard let item = toSectionItem(msg) else { return }
|
guard let item = toSectionItem(msg) else { return }
|
||||||
let ts = timestampFrom(item: item)
|
let ts = timestampFrom(item: item)
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
|
|
||||||
|
// 去重:如果 clientMsgID 已存在(本地占位消息),跳过监听器追加
|
||||||
|
if let clientMsgID = msg.clientMsgID, !clientMsgID.isEmpty,
|
||||||
|
items.contains(where: { $0.chatMessage?.id == clientMsgID }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let showTime = items.isEmpty || ts - lastTimeGap >= timeGapThreshold
|
let showTime = items.isEmpty || ts - lastTimeGap >= timeGapThreshold
|
||||||
lastTimeGap = ts
|
lastTimeGap = ts
|
||||||
items.append(showTime ? setShowTime(item, true) : item)
|
items.append(showTime ? setShowTime(item, true) : item)
|
||||||
|
|
@ -284,3 +317,30 @@ final class GroupChatViewModel {
|
||||||
return displayStr
|
return displayStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ChatSectionItem Helpers
|
||||||
|
extension ChatSectionItem {
|
||||||
|
/// 提取 ChatMessage(仅用于有消息的 case)
|
||||||
|
var chatMessage: ChatMessage? {
|
||||||
|
switch self {
|
||||||
|
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
|
||||||
|
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
|
||||||
|
return m
|
||||||
|
case .notification: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用给定 ChatMessage 重建 case
|
||||||
|
static func with(_ msg: ChatMessage) -> ChatSectionItem {
|
||||||
|
if !msg.imageUrl.isEmpty {
|
||||||
|
return msg.isSelf ? .imageSend(msg) : .imageReceived(msg)
|
||||||
|
}
|
||||||
|
if msg.content.hasPrefix("js_emoji:") {
|
||||||
|
return msg.isSelf ? .emojiSend(msg) : .emojiReceived(msg)
|
||||||
|
}
|
||||||
|
if !msg.voiceUrl.isEmpty {
|
||||||
|
return msg.isSelf ? .voiceSend(msg) : .voiceReceived(msg)
|
||||||
|
}
|
||||||
|
return msg.isSelf ? .send(msg) : .received(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
//
|
||||||
|
// GroupSettingVC.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
// Created by 八条 on 2026/6/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
import RxDataSources
|
||||||
|
import ObjectMapper
|
||||||
|
|
||||||
|
class GroupSettingVC: BaseViewController {
|
||||||
|
|
||||||
|
fileprivate var rootView: GroupSettingView!
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
rootView = GroupSettingView(frame: UIScreen.main.bounds)
|
||||||
|
view = rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
private var viewModel: GroupSettingViewModel
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Do any additional setup after loading the view.
|
||||||
|
reactiveAction()
|
||||||
|
requestGroupInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
private func reactiveAction() {
|
||||||
|
// 更换名称
|
||||||
|
rootView.groupNameView.rx.tapGesture.subscribe { _ in
|
||||||
|
guard let model = self.viewModel.groupModel, model.is_owner else { return }
|
||||||
|
let vc = TextInputViewController(title: "圈子名称", maxLength: 10, initialText: model.name) { text in
|
||||||
|
self.requestEditName(text)
|
||||||
|
}
|
||||||
|
self.present(vc, animated: true, completion: nil)
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 修改描述
|
||||||
|
rootView.groupDescView.rx.tapGesture.subscribe { _ in
|
||||||
|
guard let model = self.viewModel.groupModel, model.is_owner else { return }
|
||||||
|
let vc = TextInputViewController(title: "圈子描述", maxLength: 40, initialText: model.description) { text in
|
||||||
|
self.requestEditDesc(text)
|
||||||
|
}
|
||||||
|
self.present(vc, animated: true, completion: nil)
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 更换图标
|
||||||
|
rootView.groupIconView.rx.tapGesture.subscribe { _ in
|
||||||
|
guard let model = self.viewModel.groupModel, model.is_owner else { return }
|
||||||
|
let vc = GroupIconListVC(iconIndex: "\(model.icon_index)")
|
||||||
|
vc.onSelectIcon = { index in
|
||||||
|
self.requestChangeIcon(index)
|
||||||
|
}
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 审核开关
|
||||||
|
rootView.switchBtn.rx.isOn
|
||||||
|
.subscribe(onNext: { isOn in
|
||||||
|
guard let model = self.viewModel.groupModel, model.is_owner else { return }
|
||||||
|
self.requestChangeReview(isOn)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
rootView.tagView.rx.tapGesture.subscribe { _ in
|
||||||
|
guard let model = self.viewModel.groupModel, model.is_owner else { return }
|
||||||
|
GroupTagListView.show(selectedTagList: model.labels) { tagList in
|
||||||
|
guard let model = self.viewModel.groupModel,
|
||||||
|
model.is_owner,
|
||||||
|
let list = tagList else { return }
|
||||||
|
self.requestEditLabels(labels: list)
|
||||||
|
}
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 邀请成员
|
||||||
|
rootView.inviteView.rx.tapGesture.subscribe(onNext: { _ in
|
||||||
|
guard let model = self.viewModel.groupModel else { return }
|
||||||
|
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": model.toJSON()])
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API
|
||||||
|
private func requestGroupInfo() {
|
||||||
|
DLToast.showLoading()
|
||||||
|
GroupService.groupInfoByKey(viewModel.groupId).subscribe { response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
guard let model = response.model else { return }
|
||||||
|
self.viewModel.groupModel = model
|
||||||
|
self.rootView.setupData(model)
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestChangeIcon(_ iconIndex: Int) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
GroupService.changeIcon(requestData: ["group_key": viewModel.groupId, "icon_index": iconIndex]).subscribe { response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
self.requestGroupInfo()
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestChangeReview(_ review: Bool) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
GroupService.changeReview(requestData: ["group_key": viewModel.groupId, "review": review]).subscribe { response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
self.requestGroupInfo()
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestEditName(_ name: String) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
GroupService.editName(requestData: ["group_key": viewModel.groupId, "group_name": name]).subscribe { response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
self.requestGroupInfo()
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestEditDesc(_ desc: String) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
GroupService.editDesc(requestData: ["group_key": viewModel.groupId, "description": desc]).subscribe { response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
self.requestGroupInfo()
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestEditLabels(labels: [String]) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
GroupService.editLabels(requestData: ["group_key": viewModel.groupId, "labels": labels]).subscribe { response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
self.requestGroupInfo()
|
||||||
|
}.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
init(groupId: String) {
|
||||||
|
self.viewModel = GroupSettingViewModel(groupId: groupId)
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,651 @@
|
||||||
|
//
|
||||||
|
// GroupSettingView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
// Created by 八条 on 2026/6/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
import TagListView
|
||||||
|
|
||||||
|
class GroupSettingView: UIView {
|
||||||
|
|
||||||
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
func setupData(_ model: GroupInfoModel) {
|
||||||
|
navTitleLabel.text = model.name
|
||||||
|
groupNameLab.text = model.name
|
||||||
|
groupIcon.image = model.groupIcon
|
||||||
|
groupDescLab.text = model.description.isEmpty ? "暂无描述" : model.description
|
||||||
|
switchBtn.isOn = model.review
|
||||||
|
tagListView.removeAllTags()
|
||||||
|
tagListView.addTags(model.labels)
|
||||||
|
tagListView.tagViews.forEach {
|
||||||
|
$0.layer.cornerRadius = 4
|
||||||
|
}
|
||||||
|
tagListView.invalidateIntrinsicContentSize() // 通知系统重新算高
|
||||||
|
|
||||||
|
auditSwitchView.isHidden = !model.is_owner
|
||||||
|
groupNameEditIcon.isHidden = !model.is_owner
|
||||||
|
groupIconArrowIcon.isHidden = !model.is_owner
|
||||||
|
groupDescEditIcon.isHidden = !model.is_owner
|
||||||
|
groupTagArrowIcon.isHidden = !model.is_owner
|
||||||
|
|
||||||
|
auditMemberView.isHidden = !model.is_owner
|
||||||
|
removeMemberView.isHidden = !model.is_owner
|
||||||
|
dismissGroupView.isHidden = !model.is_owner
|
||||||
|
leaveGroupView.isHidden = model.is_owner
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupRx() {
|
||||||
|
backBtn.rx.tap.subscribe(onNext: { _ in
|
||||||
|
AppRouter.shared.popOrDismiss()
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
addSubview(navBgView)
|
||||||
|
addSubview(navBarView)
|
||||||
|
navBarView.addSubview(navTitleLabel)
|
||||||
|
navBarView.addSubview(backBtn)
|
||||||
|
|
||||||
|
addSubview(scrollView)
|
||||||
|
scrollView.addSubview(scrollContentView)
|
||||||
|
scrollContentView.addSubview(infoView)
|
||||||
|
infoView.addSubview(infoStackView)
|
||||||
|
|
||||||
|
scrollContentView.addSubview(groupManagerTitleLab)
|
||||||
|
scrollContentView.addSubview(groupManagerView)
|
||||||
|
groupManagerView.addSubview(groupManagerStackView)
|
||||||
|
|
||||||
|
navBgView.layoutChain
|
||||||
|
.edges(excludingEdge: .bottom)
|
||||||
|
.heightToWidth(160/375)
|
||||||
|
|
||||||
|
navBarView.layoutChain
|
||||||
|
.edges(excludingEdge: .bottom)
|
||||||
|
.height(kNaviHeight)
|
||||||
|
|
||||||
|
navTitleLabel.layoutChain
|
||||||
|
.top(kStatusBarHeight + 12)
|
||||||
|
.centerY(backBtn)
|
||||||
|
.centerX()
|
||||||
|
|
||||||
|
backBtn.layoutChain
|
||||||
|
.centerY(navTitleLabel)
|
||||||
|
.left(15)
|
||||||
|
.width(24)
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
scrollView.layoutChain
|
||||||
|
.topToBottomOfView(navBarView)
|
||||||
|
.edges(excludingEdge: .top)
|
||||||
|
|
||||||
|
scrollContentView.layoutChain
|
||||||
|
.edges()
|
||||||
|
.widthToView(scrollView)
|
||||||
|
|
||||||
|
infoView.layoutChain
|
||||||
|
.top(10)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
infoStackView.layoutChain
|
||||||
|
.edges()
|
||||||
|
|
||||||
|
groupNameView.layoutChain
|
||||||
|
.height(60)
|
||||||
|
|
||||||
|
groupIconView.layoutChain
|
||||||
|
.height(60)
|
||||||
|
|
||||||
|
auditSwitchView.layoutChain
|
||||||
|
.height(57)
|
||||||
|
|
||||||
|
groupManagerTitleLab.layoutChain
|
||||||
|
.topToBottomOfView(infoView, offset: 20)
|
||||||
|
.left(15)
|
||||||
|
|
||||||
|
groupManagerView.layoutChain
|
||||||
|
.topToBottomOfView(groupManagerTitleLab, offset: 15)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
.bottom(30)
|
||||||
|
|
||||||
|
groupManagerStackView.layoutChain
|
||||||
|
.edges()
|
||||||
|
|
||||||
|
inviteView.layoutChain.height(57)
|
||||||
|
auditMemberView.layoutChain.height(57)
|
||||||
|
removeMemberView.layoutChain.height(57)
|
||||||
|
dismissGroupView.layoutChain.height(57)
|
||||||
|
leaveGroupView.layoutChain.height(57)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var navBgView: UIImageView = {
|
||||||
|
let iv = UIImageView()
|
||||||
|
iv.image = UIImage(named: "Common/navBar_bg_2")
|
||||||
|
iv.contentMode = .scaleAspectFill
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var navBarView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var navTitleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 18, weight: .medium)
|
||||||
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var backBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setImage(UIImage(named: "Common/back"), for: .normal)
|
||||||
|
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var scrollView: UIScrollView = {
|
||||||
|
let view = UIScrollView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
// view.showsVerticalScrollIndicator = false
|
||||||
|
// view.bounces = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var scrollContentView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - 圈子信息
|
||||||
|
lazy var infoView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
|
||||||
|
view.cornerRadius = 10
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var infoStackView: UIStackView = {
|
||||||
|
let view = UIStackView(arrangedSubviews: [groupNameView, groupIconView, groupDescView, auditSwitchView, tagView])
|
||||||
|
view.axis = .vertical
|
||||||
|
view.alignment = .fill
|
||||||
|
view.distribution = .fill
|
||||||
|
view.spacing = 0
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 圈子名字
|
||||||
|
lazy var groupNameView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "圈子名称"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
view.addSubview(groupNameEditIcon)
|
||||||
|
groupNameEditIcon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(20)
|
||||||
|
.height(20)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
view.addSubview(groupNameLab)
|
||||||
|
groupNameLab.layoutChain
|
||||||
|
.leftToRightOfView(titleLab, offset: 20)
|
||||||
|
.rightToLeftOfView(groupNameEditIcon, offset: -17, relation: .greaterThanOrEqual)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupNameLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupNameEditIcon: UIImageView = {
|
||||||
|
let view = UIImageView(image: UIImage(named: "Group/edit"))
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 圈子图标
|
||||||
|
lazy var groupIconView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "圈子图标"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
view.addSubview(groupIconArrowIcon)
|
||||||
|
groupIconArrowIcon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
view.addSubview(groupIcon)
|
||||||
|
groupIcon.layoutChain
|
||||||
|
.leftToRightOfView(titleLab, offset: 20)
|
||||||
|
.centerY()
|
||||||
|
.width(40)
|
||||||
|
.heightToWidth(1)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupIcon: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.contentMode = .scaleAspectFill
|
||||||
|
view.cornerRadius = 10
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupIconArrowIcon: UIImageView = {
|
||||||
|
let view = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 圈子描述
|
||||||
|
lazy var groupDescView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "圈子描述"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.top(22)
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
view.addSubview(groupDescEditIcon)
|
||||||
|
groupDescEditIcon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(20)
|
||||||
|
.height(20)
|
||||||
|
.centerY(titleLab)
|
||||||
|
|
||||||
|
view.addSubview(groupDescLab)
|
||||||
|
groupDescLab.layoutChain
|
||||||
|
.top(20)
|
||||||
|
.leftToRightOfView(titleLab, offset: 20)
|
||||||
|
.rightToLeftOfView(groupDescEditIcon, offset: -17)
|
||||||
|
.bottom(20)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupDescLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupDescEditIcon: UIImageView = {
|
||||||
|
let view = UIImageView(image: UIImage(named: "Group/edit"))
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 审核开关
|
||||||
|
lazy var auditSwitchView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.clipsToBounds = true
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "开启审核"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
view.addSubview(switchBtn)
|
||||||
|
switchBtn.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.centerY()
|
||||||
|
.width(51)
|
||||||
|
.height(30)
|
||||||
|
|
||||||
|
view.isHidden = true
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var switchBtn: UISwitch = {
|
||||||
|
let view = UISwitch()
|
||||||
|
view.isOn = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
lazy var tagView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "圈子标签"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.top(20)
|
||||||
|
|
||||||
|
view.addSubview(groupTagArrowIcon)
|
||||||
|
groupTagArrowIcon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY(titleLab)
|
||||||
|
|
||||||
|
view.addSubview(tagListView)
|
||||||
|
tagListView.layoutChain
|
||||||
|
.top(15)
|
||||||
|
.leftToRightOfView(titleLab, offset: 23)
|
||||||
|
.height(27, relation: .greaterThanOrEqual)
|
||||||
|
.rightToLeftOfView(groupTagArrowIcon, offset: -17)
|
||||||
|
|
||||||
|
let tipsLab = UILabel()
|
||||||
|
tipsLab.text = "如选择为私密圈子,将不能被分享到探索和被搜索。"
|
||||||
|
tipsLab.textColor = ThemeManager.shared.color.contentColor
|
||||||
|
tipsLab.font = .systemFont(ofSize: 10, weight: .regular)
|
||||||
|
view.addSubview(tipsLab)
|
||||||
|
tipsLab.layoutChain
|
||||||
|
.topToBottomOfView(tagListView, offset: 10)
|
||||||
|
.leftToView(titleLab)
|
||||||
|
.bottom(20)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var tagListView: TagListView = {
|
||||||
|
let view = TagListView()
|
||||||
|
view.textFont = UIFont.systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.textColor = UIColor(hexStr: "#16B3FF")
|
||||||
|
view.tagBackgroundColor = UIColor(hexStr: "#E3F6FF")
|
||||||
|
view.paddingX = 20 // 水平内边距
|
||||||
|
view.paddingY = 6 // 垂直内边距
|
||||||
|
view.alignment = .left // 对齐
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupTagArrowIcon: UIImageView = {
|
||||||
|
let view = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - 圈子管理
|
||||||
|
lazy var groupManagerTitleLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "圈子管理"
|
||||||
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
label.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupManagerView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
|
||||||
|
view.cornerRadius = 10
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupManagerStackView: UIStackView = {
|
||||||
|
let view = UIStackView(arrangedSubviews: [inviteView, auditMemberView, removeMemberView, dismissGroupView, leaveGroupView])
|
||||||
|
view.axis = .vertical
|
||||||
|
view.alignment = .fill
|
||||||
|
view.distribution = .fill
|
||||||
|
view.spacing = 0
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 邀请成员
|
||||||
|
lazy var inviteView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "邀请成员"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.top(20)
|
||||||
|
|
||||||
|
let icon = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.addSubview(icon)
|
||||||
|
icon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 审核成员
|
||||||
|
lazy var auditMemberView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isHidden = true
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "审核成员"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.top(20)
|
||||||
|
|
||||||
|
let icon = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.addSubview(icon)
|
||||||
|
icon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 移除成员
|
||||||
|
lazy var removeMemberView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isHidden = true
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "移除成员"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.top(20)
|
||||||
|
|
||||||
|
let icon = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.addSubview(icon)
|
||||||
|
icon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 解散圈子
|
||||||
|
lazy var dismissGroupView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isHidden = true
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "解散圈子"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.top(20)
|
||||||
|
|
||||||
|
let icon = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.addSubview(icon)
|
||||||
|
icon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 退出圈子
|
||||||
|
lazy var leaveGroupView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isHidden = true
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = "退出圈子"
|
||||||
|
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
titleLab.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.width(50)
|
||||||
|
.top(20)
|
||||||
|
|
||||||
|
let icon = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.addSubview(icon)
|
||||||
|
icon.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
backgroundColor = .white
|
||||||
|
setupUI()
|
||||||
|
setupRx()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// GroupSettingViewModel.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
// Created by 八条 on 2026/6/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GroupSettingViewModel {
|
||||||
|
|
||||||
|
let groupId: String
|
||||||
|
|
||||||
|
var groupModel: GroupInfoModel?
|
||||||
|
|
||||||
|
init(groupId: String) {
|
||||||
|
self.groupId = groupId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,303 @@
|
||||||
|
//
|
||||||
|
// GroupTagListView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
// Created by 八条 on 2026/6/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
|
||||||
|
class GroupTagListView: UIView {
|
||||||
|
|
||||||
|
private static let shared = GroupTagListView(frame: CGRect(origin: .zero, size: kScreenSize))
|
||||||
|
|
||||||
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
private let tagList = ["私密", "聚会", "运动", "美食",
|
||||||
|
"旅行", "学习", "自驾", "游戏"]
|
||||||
|
|
||||||
|
private var selectedTagList: [String] = [] {
|
||||||
|
didSet {
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 完成选中进行回调
|
||||||
|
private var completion: (([String]?) -> Void)?
|
||||||
|
|
||||||
|
@objc func tap() {
|
||||||
|
completion?(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var bgView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .black.withAlphaComponent(0.5)
|
||||||
|
view.clipsToBounds = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var infoView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .white
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "圈子标签"
|
||||||
|
label.textColor = .black
|
||||||
|
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||||
|
view.addSubview(label)
|
||||||
|
label.layoutChain
|
||||||
|
.top(15)
|
||||||
|
.left(20)
|
||||||
|
|
||||||
|
view.addSubview(tableView)
|
||||||
|
view.addSubview(cancelBtn)
|
||||||
|
view.addSubview(confirmBtn)
|
||||||
|
|
||||||
|
cancelBtn.layoutChain
|
||||||
|
.left(43)
|
||||||
|
.bottom(kSafeBottomMargin + 24)
|
||||||
|
.height(40)
|
||||||
|
|
||||||
|
confirmBtn.layoutChain
|
||||||
|
.leftToRightOfView(cancelBtn, offset: 13)
|
||||||
|
.right(43)
|
||||||
|
.bottomToView(cancelBtn)
|
||||||
|
.heightToView(cancelBtn)
|
||||||
|
.widthToView(cancelBtn)
|
||||||
|
|
||||||
|
tableView.layoutChain
|
||||||
|
.topToBottomOfView(label, offset: 5)
|
||||||
|
.edgesHorzontal()
|
||||||
|
.bottomToTopOfView(cancelBtn, offset: -15)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var tableView: UITableView = {
|
||||||
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
|
tableView.backgroundColor = .white
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.estimatedRowHeight = 38
|
||||||
|
tableView.showsVerticalScrollIndicator = false
|
||||||
|
tableView.bounces = false
|
||||||
|
tableView.isScrollEnabled = false
|
||||||
|
tableView.register(TagTextCell.self)
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.delegate = self
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var confirmBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setTitle("确定", for: .normal)
|
||||||
|
btn.setTitleColor(.black, for: .normal)
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
||||||
|
btn.cornerRadius = 20
|
||||||
|
|
||||||
|
btn.rx.tap.subscribe(onNext: { _ in
|
||||||
|
if let completion = self.completion {
|
||||||
|
completion(self.selectedTagList)
|
||||||
|
}
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var cancelBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setTitle("取消", for: .normal)
|
||||||
|
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
btn.backgroundColor = .white
|
||||||
|
btn.borderWidth = 1
|
||||||
|
btn.borderColor = UIColor(hexStr: "#16B3FF")
|
||||||
|
btn.cornerRadius = 20
|
||||||
|
|
||||||
|
btn.rx.tap.subscribe(onNext: { _ in
|
||||||
|
GroupTagListView.dismiss()
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
addSubview(bgView)
|
||||||
|
bgView.addSubview(infoView)
|
||||||
|
infoView.addSubview(tableView)
|
||||||
|
infoView.addSubview(confirmBtn)
|
||||||
|
infoView.addSubview(cancelBtn)
|
||||||
|
|
||||||
|
bgView.layoutChain.edges()
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tap))
|
||||||
|
tap.delegate = self
|
||||||
|
addGestureRecognizer(tap)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
infoView.setNeedsLayout()
|
||||||
|
infoView.layoutIfNeeded()
|
||||||
|
infoView.setCornerRadius(corners: [.topLeft, .topRight], withCornerRadii: CGSize(width: 20, height: 20))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
extension GroupTagListView {
|
||||||
|
|
||||||
|
/// 显示选择弹窗
|
||||||
|
/// - Parameters:
|
||||||
|
/// - start: 显示起始点
|
||||||
|
static func show(selectedTagList: [String],
|
||||||
|
completion: @escaping (([String]?) -> Void)) {
|
||||||
|
guard let superView = kKeyWindow else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if GroupTagListView.shared.superview != nil {
|
||||||
|
GroupTagListView.shared.removeFromSuperview()
|
||||||
|
GroupTagListView.shared.bgView.frame = .zero
|
||||||
|
}
|
||||||
|
GroupTagListView.shared.selectedTagList = selectedTagList
|
||||||
|
GroupTagListView.shared.bgView.alpha = 1
|
||||||
|
|
||||||
|
superView.addSubview(GroupTagListView.shared)
|
||||||
|
superView.bringSubviewToFront(GroupTagListView.shared)
|
||||||
|
|
||||||
|
let viewHeight = kSafeBottomMargin + 24 + 40 + 40 * 8 + 40
|
||||||
|
GroupTagListView.shared.infoView.frame = CGRect(x: 0, y: kScreenHeight, width: kScreenWidth, height: viewHeight)
|
||||||
|
GroupTagListView.shared.infoView.alpha = 0
|
||||||
|
GroupTagListView.shared.completion = { tagList in
|
||||||
|
completion(tagList)
|
||||||
|
GroupTagListView.dismiss()
|
||||||
|
}
|
||||||
|
UIView.animate(withDuration: 0.25) {
|
||||||
|
GroupTagListView.shared.infoView.alpha = 1
|
||||||
|
GroupTagListView.shared.infoView.frame = CGRect(x: 0, y: kScreenHeight - viewHeight, width: kScreenWidth, height: viewHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 关闭
|
||||||
|
static func dismiss() {
|
||||||
|
guard GroupTagListView.shared.superview != nil else { return }
|
||||||
|
let viewHeight = kSafeBottomMargin + 24 + 40 + 40 * 8 + 40
|
||||||
|
UIView.animate(withDuration: 0.15) {
|
||||||
|
GroupTagListView.shared.infoView.alpha = 0
|
||||||
|
GroupTagListView.shared.infoView.frame = CGRect(x: 0, y: kScreenHeight, width: kScreenWidth, height: viewHeight)
|
||||||
|
}
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
|
||||||
|
GroupTagListView.shared.bgView.alpha = 0
|
||||||
|
} completion: { _ in
|
||||||
|
GroupTagListView.shared.removeFromSuperview()
|
||||||
|
GroupTagListView.shared.infoView.frame = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIGestureRecognizerDelegate
|
||||||
|
extension GroupTagListView: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
if let view = touch.view, !(view == self || view == bgView) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDataSource & UITableViewDelegate
|
||||||
|
extension GroupTagListView: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
tagList.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell: TagTextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
let tagText = tagList[indexPath.row]
|
||||||
|
cell.configure(text: tagText, isSelected: selectedTagList.contains(tagText))
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
let tagText = tagList[indexPath.row]
|
||||||
|
if let idx = selectedTagList.firstIndex(of: tagText) {
|
||||||
|
selectedTagList.remove(at: idx)
|
||||||
|
} else {
|
||||||
|
selectedTagList.append(tagText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TagTextCell
|
||||||
|
class TagTextCell: UITableViewCell {
|
||||||
|
|
||||||
|
func configure(text: String, isSelected: Bool) {
|
||||||
|
titleLab.text = text
|
||||||
|
titleLab.textColor = isSelected ? UIColor(hexStr: "#16B3FF") : .black
|
||||||
|
checkBoxBtn.isSelected = isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = .clear
|
||||||
|
setupSubviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupSubviews() {
|
||||||
|
contentView.addSubview(titleLab)
|
||||||
|
contentView.addSubview(checkBoxBtn)
|
||||||
|
|
||||||
|
checkBoxBtn.layoutChain
|
||||||
|
.centerY()
|
||||||
|
.right(25)
|
||||||
|
.width(24)
|
||||||
|
.heightToWidth(1.0)
|
||||||
|
|
||||||
|
titleLab.layoutChain
|
||||||
|
.edgesVertical(10)
|
||||||
|
.left(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
// Initialization code
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||||
|
super.setSelected(selected, animated: animated)
|
||||||
|
|
||||||
|
// Configure the view for the selected state
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var titleLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = .black
|
||||||
|
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var checkBoxBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Login/checkbox"), for: .normal)
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Login/selected"), for: .selected)
|
||||||
|
btn.isUserInteractionEnabled = false
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
//
|
||||||
|
// TextInputViewController.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
// Created by 八条 on 2026/6/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
|
||||||
|
/// 通用文本输入页面
|
||||||
|
/// 用法:
|
||||||
|
/// let vc = TextInputViewController(title: "编辑昵称", maxLength: 20) { text in
|
||||||
|
/// print("用户输入: \(text)")
|
||||||
|
/// }
|
||||||
|
/// present(vc, animated: true)
|
||||||
|
final class TextInputViewController: UIViewController {
|
||||||
|
|
||||||
|
private let titleText: String
|
||||||
|
private let maxLength: Int
|
||||||
|
private let confirmAction: ((String) -> Void)?
|
||||||
|
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
private let textRelay = BehaviorRelay<String>(value: "")
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
|
||||||
|
/// - Parameters:
|
||||||
|
/// - title: 页面标题
|
||||||
|
/// - maxLength: 文字输入上限(0 表示不限制)
|
||||||
|
/// - initialText: 初始文本,默认空
|
||||||
|
/// - confirmAction: 确定回调
|
||||||
|
init(title: String,
|
||||||
|
maxLength: Int = 0,
|
||||||
|
initialText: String = "",
|
||||||
|
confirmAction: ((String) -> Void)? = nil) {
|
||||||
|
self.titleText = title
|
||||||
|
self.maxLength = maxLength
|
||||||
|
self.confirmAction = confirmAction
|
||||||
|
self.textRelay.accept(initialText)
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
modalPresentationStyle = .fullScreen
|
||||||
|
modalTransitionStyle = .coverVertical
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = .white
|
||||||
|
setupUI()
|
||||||
|
setupBinding()
|
||||||
|
|
||||||
|
navTitleLabel.text = titleText
|
||||||
|
textView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UI
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
view.addSubview(navBgView)
|
||||||
|
view.addSubview(navBarView)
|
||||||
|
navBarView.addSubview(navTitleLabel)
|
||||||
|
navBarView.addSubview(backBtn)
|
||||||
|
|
||||||
|
view.addSubview(inputTextView)
|
||||||
|
inputTextView.addSubview(textView)
|
||||||
|
view.addSubview(countLabel)
|
||||||
|
view.addSubview(confirmBtn)
|
||||||
|
|
||||||
|
navBgView.layoutChain
|
||||||
|
.edges(excludingEdge: .bottom)
|
||||||
|
.heightToWidth(160/375)
|
||||||
|
|
||||||
|
navBarView.layoutChain
|
||||||
|
.edges(excludingEdge: .bottom)
|
||||||
|
.height(kNaviHeight)
|
||||||
|
|
||||||
|
navTitleLabel.layoutChain
|
||||||
|
.top(kStatusBarHeight + 12)
|
||||||
|
.centerY(backBtn)
|
||||||
|
.centerX()
|
||||||
|
|
||||||
|
backBtn.layoutChain
|
||||||
|
.centerY(navTitleLabel)
|
||||||
|
.left(15)
|
||||||
|
.width(24)
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
inputTextView.layoutChain
|
||||||
|
.topToBottomOfView(navBarView, offset: 15)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
|
||||||
|
// 输入框
|
||||||
|
textView.layoutChain
|
||||||
|
.edgesVertical(5)
|
||||||
|
.edgesHorzontal(10)
|
||||||
|
|
||||||
|
countLabel.layoutChain
|
||||||
|
.topToBottomOfView(inputTextView, offset: 5)
|
||||||
|
.rightToView(textView)
|
||||||
|
|
||||||
|
confirmBtn.layoutChain
|
||||||
|
.topToBottomOfView(inputTextView, offset: 50)
|
||||||
|
.edgesHorzontal(15).height(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binding
|
||||||
|
private func setupBinding() {
|
||||||
|
// 输入流
|
||||||
|
Observable.merge(
|
||||||
|
textView.rx.didChange.asObservable(),
|
||||||
|
textView.rx.text.map { _ in () },
|
||||||
|
textView.rx.methodInvoked(#selector(UITextView.paste(_:))).map { _ in () }
|
||||||
|
)
|
||||||
|
.throttle(.milliseconds(100), scheduler: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if self.textView.text.last == "\n" {
|
||||||
|
self.textView.text = String(self.textView.text.dropLast())
|
||||||
|
self.textView.resignFirstResponder()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = self.textView.text.count
|
||||||
|
|
||||||
|
if count > self.maxLength {
|
||||||
|
self.textView.text = String(self.textView.text.prefix(self.maxLength))
|
||||||
|
self.textView.selectedRange = NSRange(location: self.maxLength, length: 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.countLabel.text = "\(count)/\(self.maxLength)"
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
textRelay.asObservable()
|
||||||
|
.bind(to: textView.rx.text)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 确定
|
||||||
|
textView.rx.text.orEmpty.map { text in
|
||||||
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return !trimmed.isEmpty
|
||||||
|
}
|
||||||
|
.bind(to: confirmBtn.rx.isEnabled)
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
confirmBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||||
|
guard let self = self, let text = self.textView.text else { return }
|
||||||
|
self.confirmAction?(text)
|
||||||
|
self.dismiss(animated: true)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
backBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
self?.dismiss(animated: true)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
lazy var navBgView: UIImageView = {
|
||||||
|
let iv = UIImageView()
|
||||||
|
iv.image = UIImage(named: "Common/navBar_bg_2")
|
||||||
|
iv.contentMode = .scaleAspectFill
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var navBarView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var navTitleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 18, weight: .medium)
|
||||||
|
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var backBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setImage(UIImage(named: "Common/back"), for: .normal)
|
||||||
|
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var inputTextView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .white
|
||||||
|
view.cornerRadius = 4
|
||||||
|
view.borderWidth = 0.5
|
||||||
|
view.borderColor = ThemeManager.shared.color.lineColor
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var textView: UITextView = {
|
||||||
|
let tv = UITextView()
|
||||||
|
tv.font = .systemFont(ofSize: 15)
|
||||||
|
tv.textColor = ThemeManager.shared.color.titleAuxColor
|
||||||
|
tv.backgroundColor = .clear
|
||||||
|
tv.showsVerticalScrollIndicator = false
|
||||||
|
tv.isScrollEnabled = false
|
||||||
|
tv.bounces = false
|
||||||
|
tv.returnKeyType = .done
|
||||||
|
return tv
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var countLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 13)
|
||||||
|
label.textColor = UIColor(hexStr: "#999999")
|
||||||
|
label.text = maxLength > 0 ? "0/\(maxLength)" : ""
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var confirmBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setTitle("确定", for: .normal)
|
||||||
|
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
btn.cornerRadius = 25
|
||||||
|
btn.isEnabled = false
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
@ -45,4 +45,54 @@ struct GroupService {
|
||||||
.map(GroupInfoResponse.self)
|
.map(GroupInfoResponse.self)
|
||||||
.asObservable()
|
.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 修改名字
|
||||||
|
/// - Parameters:
|
||||||
|
/// - requestData:group_key group_name
|
||||||
|
static func editName(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
|
||||||
|
let api = GroupAPI.operate(opType: "changename", requestData: requestData).multiTarget
|
||||||
|
return APIProvider.request(token: api)
|
||||||
|
.map(GroupInfoResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更换图标
|
||||||
|
/// - Parameters:
|
||||||
|
/// - requestData:group_key icon_index
|
||||||
|
static func changeIcon(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
|
||||||
|
let api = GroupAPI.operate(opType: "changeicon", requestData: requestData).multiTarget
|
||||||
|
return APIProvider.request(token: api)
|
||||||
|
.map(GroupInfoResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改描述
|
||||||
|
/// - Parameters:
|
||||||
|
/// - requestData:group_key description
|
||||||
|
static func editDesc(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
|
||||||
|
let api = GroupAPI.operate(opType: "changedescription", requestData: requestData).multiTarget
|
||||||
|
return APIProvider.request(token: api)
|
||||||
|
.map(GroupInfoResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 审核开关
|
||||||
|
/// - Parameters:
|
||||||
|
/// - requestData:group_key review
|
||||||
|
static func changeReview(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
|
||||||
|
let api = GroupAPI.operate(opType: "changereview", requestData: requestData).multiTarget
|
||||||
|
return APIProvider.request(token: api)
|
||||||
|
.map(GroupInfoResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更改标签
|
||||||
|
/// - Parameters:
|
||||||
|
/// - requestData:group_key labels
|
||||||
|
static func editLabels(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
|
||||||
|
let api = GroupAPI.operate(opType: "changelabels", requestData: requestData).multiTarget
|
||||||
|
return APIProvider.request(token: api)
|
||||||
|
.map(GroupInfoResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24765" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24743"/>
|
||||||
|
<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="393" height="852"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Launch/logo" translatesAutoresizingMaskIntoConstraints="NO" id="hzJ-1y-aVD">
|
||||||
|
<rect key="frame" x="52.666666666666657" y="282" width="288" height="288"/>
|
||||||
|
</imageView>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Launch/slogan" translatesAutoresizingMaskIntoConstraints="NO" id="Dot-oD-XM4">
|
||||||
|
<rect key="frame" x="96.666666666666686" y="654" width="200" height="80"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
<color key="backgroundColor" red="0.8784313725490196" green="0.94901960784313721" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="hzJ-1y-aVD" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="9NL-SO-M2t"/>
|
||||||
|
<constraint firstItem="Dot-oD-XM4" firstAttribute="bottom" secondItem="6Tk-OE-BBY" secondAttribute="bottom" constant="-50" id="LUS-mh-ZXm"/>
|
||||||
|
<constraint firstItem="Dot-oD-XM4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="UgU-yT-gN4"/>
|
||||||
|
<constraint firstItem="hzJ-1y-aVD" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="a8U-VN-jeR"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="Launch/logo" width="288" height="288"/>
|
||||||
|
<image name="Launch/slogan" width="200" height="80"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<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="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<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="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
Loading…
Reference in New Issue