parent
59be003fc7
commit
5381d1ffda
|
|
@ -191,6 +191,11 @@
|
|||
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC185F2FD12A020041DCD1 /* VipWaivePopView.swift */; };
|
||||
30EFF2992FD65FB000EB35D4 /* VoicePlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF2982FD65FB000EB35D4 /* VoicePlayerManager.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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -387,6 +392,13 @@
|
|||
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>"; };
|
||||
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; };
|
||||
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; };
|
||||
|
|
@ -396,8 +408,6 @@
|
|||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
3070777D2FD2A214004C37CC /* lotties */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = lotties;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -759,6 +769,7 @@
|
|||
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
||||
305A76232FCA8C7000227D26 /* GroupView.swift */,
|
||||
307073E42FD18A20004C37CC /* GroupChat */,
|
||||
30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
|
||||
3062E8B82FCEAC5600CEF511 /* CreateGroup */,
|
||||
30BAB8612FCD714700C33B5C /* Join */,
|
||||
30BAB84B2FCD2FA400C33B5C /* InviteJoin */,
|
||||
|
|
@ -830,6 +841,7 @@
|
|||
3062E8C52FCFD01000CEF511 /* VipRecharge */,
|
||||
3062E8B32FCE6BA400CEF511 /* Scan */,
|
||||
30DC18592FD11E7A0041DCD1 /* Web */,
|
||||
30EFF3AD2FD7FF1400EB35D4 /* TextInput */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1079,6 +1091,25 @@
|
|||
path = Web;
|
||||
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 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1148,11 +1179,11 @@
|
|||
};
|
||||
};
|
||||
buildConfigurationList = 3E4359032FC48D26003470A5 /* Build configuration list for PBXProject "QuickLocation" */;
|
||||
developmentRegion = en;
|
||||
developmentRegion = "zh-Hans";
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
"zh-Hans",
|
||||
);
|
||||
mainGroup = 3E4358FF2FC48D26003470A5;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
|
|
@ -1216,10 +1247,14 @@
|
|||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-frameworks.sh\"\n";
|
||||
|
|
@ -1233,10 +1268,14 @@
|
|||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickLocation/Pods-QuickLocation-resources.sh\"\n";
|
||||
|
|
@ -1253,6 +1292,7 @@
|
|||
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */,
|
||||
305A768A2FCA8C7000227D26 /* Single+Response.swift in Sources */,
|
||||
305A768B2FCA8C7000227D26 /* API.swift in Sources */,
|
||||
30EFF3AE2FD7FF1400EB35D4 /* TextInputViewController.swift in Sources */,
|
||||
305A768C2FCA8C7000227D26 /* APIProvider.swift in Sources */,
|
||||
3062E8C92FCFD03B00CEF511 /* VipRechargeVC.swift in Sources */,
|
||||
305A768D2FCA8C7000227D26 /* AppNetworkConfig.swift in Sources */,
|
||||
|
|
@ -1279,6 +1319,7 @@
|
|||
305A769F2FCA8C7000227D26 /* TextContentArrowCell.swift in Sources */,
|
||||
305A76A02FCA8C7000227D26 /* TextTableViewCell.swift in Sources */,
|
||||
305A76A12FCA8C7000227D26 /* UIButton+RTL.m in Sources */,
|
||||
30EFF3A62FD7C5AF00EB35D4 /* GroupSettingVC.swift in Sources */,
|
||||
305A76A22FCA8C7000227D26 /* Array+Extension.swift in Sources */,
|
||||
305A76A32FCA8C7000227D26 /* ControlEvents+Block.swift in Sources */,
|
||||
3062E8B72FCE6BFE00CEF511 /* ScanView.swift in Sources */,
|
||||
|
|
@ -1303,6 +1344,7 @@
|
|||
305A76B42FCA8C7000227D26 /* UINavigationController+FDFullscreenPopGesture.m in Sources */,
|
||||
305A76B52FCA8C7000227D26 /* UITableView+Extension.swift in Sources */,
|
||||
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
|
||||
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
|
||||
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
|
||||
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
|
||||
305A76B92FCA8C7000227D26 /* URL+Extension.swift in Sources */,
|
||||
|
|
@ -1343,6 +1385,7 @@
|
|||
305A76D62FCA8C7000227D26 /* ImagePlugin.swift in Sources */,
|
||||
305A76D72FCA8C7000227D26 /* NotEmpty.swift in Sources */,
|
||||
305A76D82FCA8C7000227D26 /* Action.swift in Sources */,
|
||||
30EFF3B02FD8122E00EB35D4 /* GroupTagListView.swift in Sources */,
|
||||
305A76D92FCA8C7000227D26 /* Action+Internal.swift in Sources */,
|
||||
305A76DA2FCA8C7000227D26 /* Button+Action.swift in Sources */,
|
||||
305A76DB2FCA8C7000227D26 /* Control+Action.swift in Sources */,
|
||||
|
|
@ -1384,6 +1427,7 @@
|
|||
305A76F32FCA8C7000227D26 /* AutoLayout+NSLayoutConstraint.swift in Sources */,
|
||||
305A76F42FCA8C7000227D26 /* AutoLayout+UIView.swift in Sources */,
|
||||
305A76F52FCA8C7000227D26 /* AutoLayoutSwift.swift in Sources */,
|
||||
30EFF3A42FD7C5A300EB35D4 /* GroupSettingView.swift in Sources */,
|
||||
305A76F62FCA8C7000227D26 /* AppRouter.swift in Sources */,
|
||||
305A76F72FCA8C7000227D26 /* RouterTarget.swift in Sources */,
|
||||
307073E52FD18A20004C37CC /* GroupChatView.swift in Sources */,
|
||||
|
|
@ -1434,6 +1478,7 @@
|
|||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
305A76812FCA8C7000227D26 /* Base */,
|
||||
30EFF3A02FD7A47900EB35D4 /* zh-Hans */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1442,6 +1487,7 @@
|
|||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
305A76832FCA8C7000227D26 /* Base */,
|
||||
30EFF3A12FD7A47900EB35D4 /* zh-Hans */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1463,6 +1509,12 @@
|
|||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QuickLocation/Info.plist;
|
||||
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_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
|
|
@ -1505,6 +1557,12 @@
|
|||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QuickLocation/Info.plist;
|
||||
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_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
|
|
@ -1538,6 +1596,7 @@
|
|||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
|
|
@ -1601,6 +1660,7 @@
|
|||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,14 +7,6 @@
|
|||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>您的相机将被用于扫描二维码、拍摄照片和视频。</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>需要获取您的位置信息以在地图上显示您的位置</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>需要获取您的位置信息以在地图上显示您的位置</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>我们需要使用您的麦克风,以便您使用麦克风进行音频录制。</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>douyu.otf</string>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ enum Route: String {
|
|||
case vipRights = "vipRights"
|
||||
/// 群聊
|
||||
case groupChat = "groupChat"
|
||||
/// 圈子设置
|
||||
case groupSetting = "groupSetting"
|
||||
}
|
||||
|
||||
extension Route: RouterTarget {
|
||||
|
|
@ -149,10 +151,17 @@ extension AppRouter: AppRouterProtocol {
|
|||
VipRightsVC()
|
||||
}
|
||||
|
||||
// MARK: - 群聊
|
||||
AppRouter.register(Route.groupChat) { url, parameters in
|
||||
let groupId = parameters["groupId"].safeString
|
||||
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
|
||||
public var contentColor = UIColor(hexStr: "#999999")
|
||||
/// 线条
|
||||
public var lineColor = UIColor(hexStr: "#F2F2F2")
|
||||
public var lineColor = UIColor(hexStr: "#EEEEEE")
|
||||
/// 背景色
|
||||
public var backgroundColor = UIColor(hexStr: "#F2F2F2")
|
||||
/// 商品卡片背景色
|
||||
|
|
|
|||
|
|
@ -105,9 +105,13 @@ struct GroupInfoModel: Mappable, Equatable {
|
|||
var groupIcon: UIImage {
|
||||
UIImage(named: "GroupIcon/\(icon_index)") ?? UIImage()
|
||||
}
|
||||
/// 标签
|
||||
var labels: [String] = []
|
||||
/// 审核开关
|
||||
var review: Bool = false
|
||||
/// 人数
|
||||
var people_no: Int = 0
|
||||
///
|
||||
/// 描述
|
||||
var description: String = ""
|
||||
/// 会员等级
|
||||
var level: String = ""
|
||||
|
|
@ -121,7 +125,10 @@ struct GroupInfoModel: Mappable, Equatable {
|
|||
is_owner <- map["is_owner"]
|
||||
name <- map["name"]
|
||||
icon_index <- map["icon_index"]
|
||||
labels <- map["labels"]
|
||||
level <- map["level"]
|
||||
review <- map["review"]
|
||||
description <- map["description"]
|
||||
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 {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
rootView.selectedIndex = indexPath.row + 1
|
||||
onSelectIcon?(rootView.selectedIndex)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import RxDataSources
|
|||
import OpenIMSDK
|
||||
import AVFoundation
|
||||
import AudioToolbox
|
||||
import HXPHPicker
|
||||
import IQKeyboardManagerSwift
|
||||
|
||||
final class GroupChatVC: BaseViewController {
|
||||
|
||||
|
|
@ -45,6 +47,49 @@ final class GroupChatVC: BaseViewController {
|
|||
setupMessageListener()
|
||||
setupVoiceRecording()
|
||||
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() {
|
||||
|
|
@ -89,19 +134,6 @@ final class GroupChatVC: BaseViewController {
|
|||
|
||||
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() {
|
||||
let count = dataSource.sectionModels.first?.items.count ?? 0
|
||||
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>> = {
|
||||
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
||||
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||
|
|
@ -183,25 +171,58 @@ final class GroupChatVC: BaseViewController {
|
|||
})
|
||||
.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 }
|
||||
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)
|
||||
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.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)
|
||||
|
||||
// 键盘按钮
|
||||
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)
|
||||
.subscribe(onNext: { [weak self] name in
|
||||
guard let self = self, let idx = UITableViewCell.emojiFileNames.firstIndex(of: name) else { return }
|
||||
|
|
@ -209,7 +230,17 @@ final class GroupChatVC: BaseViewController {
|
|||
})
|
||||
.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(
|
||||
rootView.sendBtn.rx.tap.map { [weak self] _ in self?.rootView.textField.text ?? "" },
|
||||
|
|
@ -222,6 +253,65 @@ final class GroupChatVC: BaseViewController {
|
|||
sendText
|
||||
.bind(to: viewModel.input.sendMessage)
|
||||
.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
|
||||
|
|
@ -331,6 +421,56 @@ final class GroupChatVC: BaseViewController {
|
|||
self.viewModel.loadMessages()
|
||||
}.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
|
||||
|
|
@ -345,3 +485,97 @@ private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
|
|||
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 content: String
|
||||
let voiceUrl: String
|
||||
let imageUrl: String
|
||||
var imageUrl: String
|
||||
let imageWidth: CGFloat
|
||||
let imageHeight: CGFloat
|
||||
let timestamp: TimeInterval
|
||||
var showTime: Bool = false
|
||||
var isUploading: Bool = false
|
||||
}
|
||||
|
||||
class GroupChatView: UIView {
|
||||
|
|
@ -44,54 +45,22 @@ class GroupChatView: UIView {
|
|||
var onVoiceRecordState: ((VoiceRecordState) -> Void)?
|
||||
|
||||
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() {
|
||||
let needsReset = !emojiPanelView.isHidden || !voiceRecordView.isHidden || !voiceRecordView.isHidden
|
||||
func dismissAllPanels(excludeTextField: Bool = false) {
|
||||
let needsReset = !emojiPanelView.isHidden
|
||||
|| !voiceRecordView.isHidden
|
||||
|| textField.isFirstResponder
|
||||
|
||||
guard needsReset else { return }
|
||||
emojiPanelView.isHidden = true
|
||||
voiceRecordView.isHidden = true
|
||||
textField.resignFirstResponder()
|
||||
if !excludeTextField { textField.resignFirstResponder() }
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20)
|
||||
self.voiceRecordView.layoutChain.bottom(-252)
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1226,15 +1195,33 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
|||
// MARK: - 发送的图片消息
|
||||
final class ImageSendMsgCell: UITableViewCell {
|
||||
|
||||
var onImageTap: (() -> Void)?
|
||||
|
||||
func configure(_ msg: ChatMessage) {
|
||||
timeLabel.isHidden = !msg.showTime
|
||||
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||
avatarView.image = msg.avatar
|
||||
if !msg.imageUrl.isEmpty {
|
||||
// 优先加载本地文件路径,否则走网络加载
|
||||
if let localImage = UIImage(contentsOfFile: msg.imageUrl) {
|
||||
photoView.image = localImage
|
||||
} else {
|
||||
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 {
|
||||
guard w > 0, h > 0 else { return CGSize(width: 160, height: 160) }
|
||||
|
|
@ -1276,12 +1263,13 @@ final class ImageSendMsgCell: UITableViewCell {
|
|||
return iv
|
||||
}()
|
||||
|
||||
private let photoView: UIImageView = {
|
||||
lazy var photoView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.cornerRadius = 8
|
||||
iv.clipsToBounds = true
|
||||
iv.backgroundColor = UIColor(hexStr: "#F0F0F0")
|
||||
iv.isUserInteractionEnabled = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
|
|
@ -1290,8 +1278,13 @@ final class ImageSendMsgCell: UITableViewCell {
|
|||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.addSubview(timeLabel)
|
||||
contentView.addSubview(avatarView)
|
||||
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()
|
||||
avatarView.layoutChain
|
||||
|
|
@ -1312,6 +1305,8 @@ final class ImageSendMsgCell: UITableViewCell {
|
|||
// MARK: - 收到的图片消息
|
||||
final class ImageReceivedMsgCell: UITableViewCell {
|
||||
|
||||
var onImageTap: (() -> Void)?
|
||||
|
||||
func configure(_ msg: ChatMessage) {
|
||||
timeLabel.isHidden = !msg.showTime
|
||||
timeLabel.text = msg.showTime ? formatTime(msg.timestamp) : nil
|
||||
|
|
@ -1323,6 +1318,8 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
|||
photoView.layoutChain.width(msg.imageWidth).height(msg.imageHeight)
|
||||
}
|
||||
|
||||
@objc private func onTap() { onImageTap?() }
|
||||
|
||||
private static func displaySize(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
|
||||
|
|
@ -1370,12 +1367,13 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
private let photoView: UIImageView = {
|
||||
lazy var photoView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.contentMode = .scaleAspectFill
|
||||
iv.cornerRadius = 8
|
||||
iv.clipsToBounds = true
|
||||
iv.backgroundColor = UIColor(hexStr: "#F0F0F0")
|
||||
iv.isUserInteractionEnabled = true
|
||||
return iv
|
||||
}()
|
||||
|
||||
|
|
@ -1388,6 +1386,9 @@ final class ImageReceivedMsgCell: UITableViewCell {
|
|||
contentView.addSubview(photoView)
|
||||
contentView.addSubview(avatarView)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(onTap))
|
||||
photoView.addGestureRecognizer(tap)
|
||||
|
||||
timeLabel.layoutChain.top().centerX()
|
||||
|
||||
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
|
||||
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
||||
guard let item = toSectionItem(msg) else { return }
|
||||
let ts = timestampFrom(item: item)
|
||||
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
|
||||
lastTimeGap = ts
|
||||
items.append(showTime ? setShowTime(item, true) : item)
|
||||
|
|
@ -284,3 +317,30 @@ final class GroupChatViewModel {
|
|||
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)
|
||||
.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