|
|
@ -96,6 +96,7 @@
|
||||||
305A76DE2FCA8C7000227D26 /* ObservableType+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */; };
|
305A76DE2FCA8C7000227D26 /* ObservableType+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */; };
|
||||||
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */; };
|
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */; };
|
||||||
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76232FCA8C7000227D26 /* GroupView.swift */; };
|
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76232FCA8C7000227D26 /* GroupView.swift */; };
|
||||||
|
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; };
|
||||||
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76242FCA8C7000227D26 /* GroupViewController.swift */; };
|
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76242FCA8C7000227D26 /* GroupViewController.swift */; };
|
||||||
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; };
|
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; };
|
||||||
305A76E32FCA8C7000227D26 /* GroupMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76272FCA8C7000227D26 /* GroupMemberView.swift */; };
|
305A76E32FCA8C7000227D26 /* GroupMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76272FCA8C7000227D26 /* GroupMemberView.swift */; };
|
||||||
|
|
@ -151,7 +152,6 @@
|
||||||
305A771C2FCA8C7000227D26 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A767E2FCA8C7000227D26 /* AppDelegate.swift */; };
|
305A771C2FCA8C7000227D26 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A767E2FCA8C7000227D26 /* AppDelegate.swift */; };
|
||||||
305A771D2FCA8C7000227D26 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76862FCA8C7000227D26 /* ViewController.swift */; };
|
305A771D2FCA8C7000227D26 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76862FCA8C7000227D26 /* ViewController.swift */; };
|
||||||
305A771E2FCA8C7000227D26 /* adhoc.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 305A74D12FCA8C7000227D26 /* adhoc.xcconfig */; };
|
305A771E2FCA8C7000227D26 /* adhoc.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 305A74D12FCA8C7000227D26 /* adhoc.xcconfig */; };
|
||||||
305A771F2FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */; };
|
|
||||||
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */; };
|
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */; };
|
||||||
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 305A767F2FCA8C7000227D26 /* Assets.xcassets */; };
|
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 305A767F2FCA8C7000227D26 /* Assets.xcassets */; };
|
||||||
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */; };
|
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */; };
|
||||||
|
|
@ -296,6 +296,9 @@
|
||||||
557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; };
|
557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; };
|
||||||
55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; };
|
55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; };
|
||||||
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; };
|
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; };
|
||||||
|
55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */; };
|
||||||
|
55B217A13022F0A000784702 /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A03022F0A000784701 /* FontManager.swift */; };
|
||||||
|
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; };
|
||||||
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
||||||
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
|
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
|
||||||
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; };
|
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; };
|
||||||
|
|
@ -386,7 +389,6 @@
|
||||||
305A752F2FCA8C7000227D26 /* SystemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemResponse.swift; sourceTree = "<group>"; };
|
305A752F2FCA8C7000227D26 /* SystemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemResponse.swift; sourceTree = "<group>"; };
|
||||||
305A75312FCA8C7000227D26 /* ImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlugin.swift; sourceTree = "<group>"; };
|
305A75312FCA8C7000227D26 /* ImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlugin.swift; sourceTree = "<group>"; };
|
||||||
305A75322FCA8C7000227D26 /* NotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEmpty.swift; sourceTree = "<group>"; };
|
305A75322FCA8C7000227D26 /* NotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEmpty.swift; sourceTree = "<group>"; };
|
||||||
305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "D-DIN-PRO-500-Medium.otf"; sourceTree = "<group>"; };
|
|
||||||
305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "zihunbiantaoti-r.ttf"; sourceTree = "<group>"; };
|
305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "zihunbiantaoti-r.ttf"; sourceTree = "<group>"; };
|
||||||
305A761A2FCA8C7000227D26 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
|
305A761A2FCA8C7000227D26 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
|
||||||
305A761B2FCA8C7000227D26 /* Action+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+Internal.swift"; sourceTree = "<group>"; };
|
305A761B2FCA8C7000227D26 /* Action+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+Internal.swift"; sourceTree = "<group>"; };
|
||||||
|
|
@ -397,6 +399,7 @@
|
||||||
305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableType+ObjectMapper.swift"; sourceTree = "<group>"; };
|
305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableType+ObjectMapper.swift"; sourceTree = "<group>"; };
|
||||||
305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+ObjectMapper.swift"; sourceTree = "<group>"; };
|
305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+ObjectMapper.swift"; sourceTree = "<group>"; };
|
||||||
305A76232FCA8C7000227D26 /* GroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupView.swift; sourceTree = "<group>"; };
|
305A76232FCA8C7000227D26 /* GroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupView.swift; sourceTree = "<group>"; };
|
||||||
|
55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; };
|
||||||
305A76242FCA8C7000227D26 /* GroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewController.swift; sourceTree = "<group>"; };
|
305A76242FCA8C7000227D26 /* GroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewController.swift; sourceTree = "<group>"; };
|
||||||
305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = "<group>"; };
|
305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = "<group>"; };
|
||||||
305A76272FCA8C7000227D26 /* GroupMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView.swift; sourceTree = "<group>"; };
|
305A76272FCA8C7000227D26 /* GroupMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -605,6 +608,9 @@
|
||||||
557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = "<group>"; };
|
557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = "<group>"; };
|
||||||
55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = "<group>"; };
|
55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = "<group>"; };
|
||||||
55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = "<group>"; };
|
55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = "<group>"; };
|
||||||
|
55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = "<group>"; };
|
||||||
|
55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; };
|
||||||
|
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
|
||||||
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
||||||
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
|
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
|
||||||
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
|
|
@ -784,6 +790,7 @@
|
||||||
305A75062FCA8C7000227D26 /* Core */ = {
|
305A75062FCA8C7000227D26 /* Core */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
55B217A23022F0A000784703 /* Font */,
|
||||||
305A75042FCA8C7000227D26 /* Extension */,
|
305A75042FCA8C7000227D26 /* Extension */,
|
||||||
305A75052FCA8C7000227D26 /* Wrapper.swift */,
|
305A75052FCA8C7000227D26 /* Wrapper.swift */,
|
||||||
);
|
);
|
||||||
|
|
@ -950,7 +957,8 @@
|
||||||
305A75392FCA8C7000227D26 /* Font */ = {
|
305A75392FCA8C7000227D26 /* Font */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */,
|
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */,
|
||||||
|
55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */,
|
||||||
305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */,
|
305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */,
|
||||||
);
|
);
|
||||||
path = Font;
|
path = Font;
|
||||||
|
|
@ -989,6 +997,7 @@
|
||||||
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
|
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
|
||||||
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
||||||
305A76232FCA8C7000227D26 /* GroupView.swift */,
|
305A76232FCA8C7000227D26 /* GroupView.swift */,
|
||||||
|
55B217B03022F0A000784710 /* GroupItineraryView.swift */,
|
||||||
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
|
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
|
||||||
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
|
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
|
||||||
307073E42FD18A20004C37CC /* GroupChat */,
|
307073E42FD18A20004C37CC /* GroupChat */,
|
||||||
|
|
@ -1666,6 +1675,14 @@
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
55B217A23022F0A000784703 /* Font */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
55B217A03022F0A000784701 /* FontManager.swift */,
|
||||||
|
);
|
||||||
|
path = Font;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
B07D45692FCECE07570D9B73 /* Pods */ = {
|
B07D45692FCECE07570D9B73 /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -1744,11 +1761,12 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
305A771E2FCA8C7000227D26 /* adhoc.xcconfig in Resources */,
|
305A771E2FCA8C7000227D26 /* adhoc.xcconfig in Resources */,
|
||||||
305A771F2FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf in Resources */,
|
|
||||||
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */,
|
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */,
|
||||||
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */,
|
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */,
|
||||||
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */,
|
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */,
|
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */,
|
||||||
|
55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */,
|
||||||
|
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */,
|
||||||
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
|
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
@ -1911,6 +1929,7 @@
|
||||||
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */,
|
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */,
|
||||||
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */,
|
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */,
|
||||||
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */,
|
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */,
|
||||||
|
55B217A13022F0A000784702 /* FontManager.swift in Sources */,
|
||||||
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */,
|
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */,
|
||||||
305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */,
|
305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */,
|
||||||
305A76B32FCA8C7000227D26 /* UILabel+Extension.swift in Sources */,
|
305A76B32FCA8C7000227D26 /* UILabel+Extension.swift in Sources */,
|
||||||
|
|
@ -1999,6 +2018,7 @@
|
||||||
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */,
|
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */,
|
||||||
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */,
|
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */,
|
||||||
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */,
|
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */,
|
||||||
|
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */,
|
||||||
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
|
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
|
||||||
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
|
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
|
||||||
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */,
|
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||||
|
|
||||||
UIFont.loadFontName("douyu.otf")
|
FontManager.registerCustomFonts()
|
||||||
UIFont.loadFontName("zihunbiantaoti-r.ttf")
|
|
||||||
setupLocation()
|
setupLocation()
|
||||||
ApiManager.shared.setup()
|
ApiManager.shared.setup()
|
||||||
AppRouter.shared.setup()
|
AppRouter.shared.setup()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "back@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "back@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47999@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47999@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 6.4 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47986@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47986@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13899@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13899@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47987@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47987@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47777@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47777@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
22
QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47778@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47778@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
22
QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47776@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47776@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "clock@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "clock@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 463 B |
|
After Width: | Height: | Size: 998 B |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "member_invite@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "member_invite@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 980 B |
BIN
QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 2.0 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47725@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 47725@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 48007@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "组 48007@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 21 KiB |
22
QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13863@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13863@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 829 B |
BIN
QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13862@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13862@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 931 B |
|
After Width: | Height: | Size: 1.5 KiB |
22
QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13862@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13862@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
22
QuickLocation/Assets.xcassets/Home/member_today_track.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "today_track@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "today_track@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
22
QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13878@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13878@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13863@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13863@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13900@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "蒙版组 13900@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 353 B |
|
After Width: | Height: | Size: 539 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1015 B |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 875 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 757 B |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
|
@ -33,20 +33,37 @@ public extension UIFont {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加载本地自定义字体
|
/// 加载本地自定义字体
|
||||||
/// - Parameter name: 字体文件名称
|
/// - Parameter name: 字体文件名称(可含扩展名,如 `xxx.ttf`)
|
||||||
|
/// - Returns: 注册后的字体 Full Name(可用于 `UIFont(name:)`)
|
||||||
public static func loadFontName(_ fileName: String) -> String? {
|
public static func loadFontName(_ fileName: String) -> String? {
|
||||||
// 字体文件资源路径
|
let nsName = fileName as NSString
|
||||||
guard let fontURL = Bundle.main.url(forResource: fileName, withExtension: nil),
|
let resource = nsName.deletingPathExtension
|
||||||
|
let ext = nsName.pathExtension
|
||||||
|
let fontURL: URL?
|
||||||
|
if ext.isEmpty {
|
||||||
|
fontURL = Bundle.main.url(forResource: fileName, withExtension: nil)
|
||||||
|
} else {
|
||||||
|
fontURL = Bundle.main.url(forResource: resource, withExtension: ext)
|
||||||
|
?? Bundle.main.url(forResource: fileName, withExtension: nil)
|
||||||
|
}
|
||||||
|
guard let fontURL,
|
||||||
let data = try? Data(contentsOf: fontURL),
|
let data = try? Data(contentsOf: fontURL),
|
||||||
let provider = CGDataProvider(data: data as CFData),
|
let provider = CGDataProvider(data: data as CFData),
|
||||||
let font = CGFont(provider) else { return nil }
|
let font = CGFont(provider) else { return nil }
|
||||||
|
|
||||||
// 注册字体
|
var error: Unmanaged<CFError>?
|
||||||
CTFontManagerRegisterGraphicsFont(font, nil)
|
CTFontManagerRegisterGraphicsFont(font, &error)
|
||||||
return String(font.fullName ?? "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Full Name 比损坏的 PostScript Name 更可靠
|
||||||
|
if let full = font.fullName as String?, !full.isEmpty {
|
||||||
|
return full
|
||||||
|
}
|
||||||
|
if let ps = font.postScriptName as String?, !ps.isEmpty, !ps.hasPrefix("-") {
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
// MARK: - Montserrat
|
// MARK: - Montserrat
|
||||||
//public extension UIFont {
|
//public extension UIFont {
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
//
|
||||||
|
// FontManager.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
// Created by 八条 on 2026/8/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 项目自定义字体样式
|
||||||
|
enum AppFont {
|
||||||
|
/// 荆南波波黑-Bold
|
||||||
|
case boboBold
|
||||||
|
/// 字魂扁黑体
|
||||||
|
case ziHunBianHei
|
||||||
|
/// 优设标题黑
|
||||||
|
case youSheBiaoTiHei
|
||||||
|
|
||||||
|
/// 候选字体名(按优先级;部分字体 PostScript 名异常,需多试)
|
||||||
|
var candidateNames: [String] {
|
||||||
|
switch self {
|
||||||
|
case .boboBold:
|
||||||
|
// TTF Full Name 为「荆南波波黑 Bold」,PostScript 名为异常的「-Bold」
|
||||||
|
return ["荆南波波黑 Bold", "荆南波波黑-Bold", "JingNanBoBoHei-Bold"]
|
||||||
|
case .ziHunBianHei:
|
||||||
|
return ["zihunbiantaoti", "字魂扁黑体"]
|
||||||
|
case .youSheBiaoTiHei:
|
||||||
|
// PostScript / Full Name 均为 YouSheBiaoTiHei
|
||||||
|
return ["YouSheBiaoTiHei", "优设标题黑"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var familyName: String? {
|
||||||
|
switch self {
|
||||||
|
case .boboBold: return "荆南波波黑"
|
||||||
|
case .ziHunBianHei: return nil
|
||||||
|
case .youSheBiaoTiHei: return "YouSheBiaoTiHei"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName: String {
|
||||||
|
switch self {
|
||||||
|
case .boboBold: return "荆南波波黑-Bold.ttf"
|
||||||
|
case .ziHunBianHei: return "zihunbiantaoti-r.ttf"
|
||||||
|
case .youSheBiaoTiHei: return "优设标题黑_猫啃网.ttf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallbackWeight: UIFont.Weight {
|
||||||
|
switch self {
|
||||||
|
case .boboBold: return .bold
|
||||||
|
case .ziHunBianHei: return .medium
|
||||||
|
case .youSheBiaoTiHei: return .bold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 统一字体管理:取字体 + 启动注册
|
||||||
|
enum FontManager {
|
||||||
|
|
||||||
|
private static var resolvedNames: [AppFont: String] = [:]
|
||||||
|
private static var didRegister = false
|
||||||
|
|
||||||
|
/// 按样式与字号取字体,失败回退系统字体
|
||||||
|
static func font(_ style: AppFont, size: CGFloat) -> UIFont {
|
||||||
|
ensureRegistered()
|
||||||
|
if let name = resolvedFontName(for: style),
|
||||||
|
let custom = UIFont(name: name, size: size) {
|
||||||
|
return custom
|
||||||
|
}
|
||||||
|
return .systemFont(ofSize: size, weight: style.fallbackWeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func boboBold(_ size: CGFloat) -> UIFont {
|
||||||
|
font(.boboBold, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ziHunBianHei(_ size: CGFloat) -> UIFont {
|
||||||
|
font(.ziHunBianHei, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func youSheBiaoTiHei(_ size: CGFloat) -> UIFont {
|
||||||
|
font(.youSheBiaoTiHei, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 注册 Bundle 内自定义字体(可重复调用)
|
||||||
|
static func registerCustomFonts() {
|
||||||
|
for style in [AppFont.boboBold, .ziHunBianHei, .youSheBiaoTiHei] {
|
||||||
|
if let registered = UIFont.loadFontName(style.fileName), !registered.isEmpty {
|
||||||
|
// 忽略异常 PostScript(如 "-Bold"),优先用可实例化的名
|
||||||
|
if !registered.hasPrefix("-"),
|
||||||
|
UIFont(name: registered, size: 12) != nil {
|
||||||
|
resolvedNames[style] = registered
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let name = resolveAvailableName(for: style) {
|
||||||
|
resolvedNames[style] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
didRegister = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func ensureRegistered() {
|
||||||
|
guard !didRegister else { return }
|
||||||
|
registerCustomFonts()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func resolvedFontName(for style: AppFont) -> String? {
|
||||||
|
if let cached = resolvedNames[style] { return cached }
|
||||||
|
if let name = resolveAvailableName(for: style) {
|
||||||
|
resolvedNames[style] = name
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func resolveAvailableName(for style: AppFont) -> String? {
|
||||||
|
for name in style.candidateNames {
|
||||||
|
if UIFont(name: name, size: 12) != nil {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let family = style.familyName {
|
||||||
|
let names = UIFont.fontNames(forFamilyName: family)
|
||||||
|
if let first = names.first {
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,7 +66,8 @@
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>douyu.otf</string>
|
<string>荆南波波黑-Bold.ttf</string>
|
||||||
|
<string>优设标题黑_猫啃网.ttf</string>
|
||||||
<string>zihunbiantaoti-r.ttf</string>
|
<string>zihunbiantaoti-r.ttf</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ final class MainTabBarController: UITabBarController {
|
||||||
private lazy var mineNav = BaseNavigationController(rootViewController: MineViewController())
|
private lazy var mineNav = BaseNavigationController(rootViewController: MineViewController())
|
||||||
|
|
||||||
private lazy var navControllers: [BaseNavigationController] = {
|
private lazy var navControllers: [BaseNavigationController] = {
|
||||||
[locationNav, exploreNav, circlesNav, mineNav]
|
[locationNav, circlesNav, exploreNav, mineNav]
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// MARK: - Custom Tab Bar
|
// MARK: - Custom Tab Bar
|
||||||
|
|
@ -25,16 +25,16 @@ final class MainTabBarController: UITabBarController {
|
||||||
image: UIImage(named: "Tabbar/location"),
|
image: UIImage(named: "Tabbar/location"),
|
||||||
selectedImage: UIImage(named: "Tabbar/location_selected")
|
selectedImage: UIImage(named: "Tabbar/location_selected")
|
||||||
),
|
),
|
||||||
QuickLocationTabBar.TabItem(
|
|
||||||
title: "探索",
|
|
||||||
image: UIImage(named: "Tabbar/explore"),
|
|
||||||
selectedImage: UIImage(named: "Tabbar/explore_selected")
|
|
||||||
),
|
|
||||||
QuickLocationTabBar.TabItem(
|
QuickLocationTabBar.TabItem(
|
||||||
title: "圈子",
|
title: "圈子",
|
||||||
image: UIImage(named: "Tabbar/group"),
|
image: UIImage(named: "Tabbar/group"),
|
||||||
selectedImage: UIImage(named: "Tabbar/group_selected")
|
selectedImage: UIImage(named: "Tabbar/group_selected")
|
||||||
),
|
),
|
||||||
|
QuickLocationTabBar.TabItem(
|
||||||
|
title: "探索",
|
||||||
|
image: UIImage(named: "Tabbar/explore"),
|
||||||
|
selectedImage: UIImage(named: "Tabbar/explore_selected")
|
||||||
|
),
|
||||||
QuickLocationTabBar.TabItem(
|
QuickLocationTabBar.TabItem(
|
||||||
title: "我的",
|
title: "我的",
|
||||||
image: UIImage(named: "Tabbar/mine"),
|
image: UIImage(named: "Tabbar/mine"),
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,14 @@ final class QuickLocationTabBar: UIView {
|
||||||
private let barContentHeight: CGFloat = 49
|
private let barContentHeight: CGFloat = 49
|
||||||
private let containerCornerRadius: CGFloat = 0
|
private let containerCornerRadius: CGFloat = 0
|
||||||
private let horizontalInset: CGFloat = 0
|
private let horizontalInset: CGFloat = 0
|
||||||
private let iconSize: CGFloat = 32
|
private let iconSize: CGFloat = 24
|
||||||
private let iconLabelSpacing: CGFloat = 4
|
private let iconLabelSpacing: CGFloat = 4
|
||||||
private let labelHeight: CGFloat = 11
|
private let labelHeight: CGFloat = 11
|
||||||
|
|
||||||
private let selectedTextColor = UIColor(hexStr: "#1A1A1A")
|
private let selectedTextColor = UIColor(hexStr: "#2A3648")
|
||||||
private let unselectedTextColor = UIColor(hexStr: "#AAAAAA")
|
private let unselectedTextColor = UIColor(hexStr: "#9BA0AF")
|
||||||
|
|
||||||
private let selectedFont = UIFont(name: "PingFangSC-Semibold", size: 11)
|
private let selectedFont = UIFont.systemFont(ofSize: 11, weight: .bold)
|
||||||
?? UIFont.systemFont(ofSize: 11, weight: .semibold)
|
|
||||||
private let unselectedFont = UIFont.systemFont(ofSize: 11, weight: .regular)
|
private let unselectedFont = UIFont.systemFont(ofSize: 11, weight: .regular)
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,611 @@
|
||||||
|
//
|
||||||
|
// UnlockCountManager.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import CoreLocation
|
||||||
|
import SwiftyUserDefaults
|
||||||
|
|
||||||
|
/// 本机解锁次数:protectedData 状态机 + 始终定位保持后台运行窗口(不上报)
|
||||||
|
final class UnlockCountManager: NSObject {
|
||||||
|
|
||||||
|
static let shared = UnlockCountManager()
|
||||||
|
|
||||||
|
private let locationManager = CLLocationManager()
|
||||||
|
private let coarseLocationManager = CLLocationManager()
|
||||||
|
private let logPrefix = "[DEBUG-UNLOCK]"
|
||||||
|
private let debugLogDefaultsKey = "unlockDebugLogs"
|
||||||
|
private let geofenceIdPrefix = "unlock.keepAlive."
|
||||||
|
private let geofenceCenterId = "unlock.keepAlive.center"
|
||||||
|
private let geofenceRadius: CLLocationDistance = 50
|
||||||
|
/// 四向卫星围栏相对中心的偏移(米),用于静止时 GPS 抖动触发
|
||||||
|
private let geofenceSatelliteOffset: CLLocationDistance = 40
|
||||||
|
/// 相对围栏中心移动超过该距离才刷新围栏网格,避免频繁重注册
|
||||||
|
private let geofenceRefreshDistance: CLLocationDistance = 40
|
||||||
|
|
||||||
|
private var didStart = false
|
||||||
|
private var isSessionLocked = false
|
||||||
|
private var hasSeenLockInSession = false
|
||||||
|
private var protectedDataPollTimer: Timer?
|
||||||
|
private var lastGeofenceCenter: CLLocationCoordinate2D?
|
||||||
|
private var lastKnownLocation: CLLocation?
|
||||||
|
private var bootBackgroundTask: UIBackgroundTaskIdentifier = .invalid
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
locationManager.delegate = self
|
||||||
|
configureLocationManager(
|
||||||
|
locationManager,
|
||||||
|
accuracy: kCLLocationAccuracyNearestTenMeters,
|
||||||
|
distanceFilter: kCLDistanceFilterNone
|
||||||
|
)
|
||||||
|
coarseLocationManager.delegate = self
|
||||||
|
configureLocationManager(
|
||||||
|
coarseLocationManager,
|
||||||
|
accuracy: 800,
|
||||||
|
distanceFilter: 100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureLocationManager(
|
||||||
|
_ manager: CLLocationManager,
|
||||||
|
accuracy: CLLocationAccuracy,
|
||||||
|
distanceFilter: CLLocationDistance
|
||||||
|
) {
|
||||||
|
manager.desiredAccuracy = accuracy
|
||||||
|
manager.distanceFilter = distanceFilter
|
||||||
|
manager.pausesLocationUpdatesAutomatically = false
|
||||||
|
manager.activityType = .otherNavigation
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
manager.showsBackgroundLocationIndicator = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public
|
||||||
|
|
||||||
|
var todayCount: Int {
|
||||||
|
rollDayIfNeeded()
|
||||||
|
return Defaults[\.unlockCountToday]
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount: Int {
|
||||||
|
rollDayIfNeeded()
|
||||||
|
return Defaults[\.unlockCountTotal]
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAlwaysLocationAuthorization: Bool {
|
||||||
|
authorizationStatus() == .authorizedAlways
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) {
|
||||||
|
let appState = UIApplication.shared.applicationState
|
||||||
|
let launchedByLocation = launchOptions?[.location] != nil
|
||||||
|
let nonActiveLaunch = appState != .active
|
||||||
|
|
||||||
|
if nonActiveLaunch {
|
||||||
|
beginBootBackgroundTask()
|
||||||
|
debugLog("coldBootNonActive appState=\(appState.rawValue) locationKey=\(launchedByLocation) bgMode=\(isLocationBackgroundModeEnabled) auth=\(authorizationStatus().rawValue) bootTask=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !didStart else {
|
||||||
|
restartLocationMonitoringIfPossible(reassertContinuous: true)
|
||||||
|
handleNonActiveLaunchIfNeeded(launchOptions)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
didStart = true
|
||||||
|
rollDayIfNeeded()
|
||||||
|
|
||||||
|
let available = UIApplication.shared.isProtectedDataAvailable
|
||||||
|
isSessionLocked = !available
|
||||||
|
dumpStoredDebugLogs()
|
||||||
|
debugLog("start auth=\(authorizationStatus().rawValue) appState=\(appState.rawValue) protected=\(available) bgMode=\(isLocationBackgroundModeEnabled)")
|
||||||
|
// 冷启动已解锁:不计数,只同步状态
|
||||||
|
if !available {
|
||||||
|
hasSeenLockInSession = true
|
||||||
|
Defaults[\.unlockWasLocked] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(protectedDataWillBecomeUnavailable),
|
||||||
|
name: UIApplication.protectedDataWillBecomeUnavailableNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(protectedDataDidBecomeAvailable),
|
||||||
|
name: UIApplication.protectedDataDidBecomeAvailableNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appDidEnterBackground),
|
||||||
|
name: UIApplication.didEnterBackgroundNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appWillTerminate),
|
||||||
|
name: UIApplication.willTerminateNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appDidBecomeActive),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
restartLocationMonitoringIfPossible(reassertContinuous: true)
|
||||||
|
handleNonActiveLaunchIfNeeded(launchOptions)
|
||||||
|
reconcileUnlockIfNeeded(reason: "coldStartAfterLock")
|
||||||
|
Defaults[\.unlockLastHeartbeatAt] = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshAuthorizationAndMonitoring() {
|
||||||
|
restartLocationMonitoringIfPossible(reassertContinuous: true)
|
||||||
|
debugLog("refresh auth=\(authorizationStatus().rawValue) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable)")
|
||||||
|
notifyChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Day roll
|
||||||
|
|
||||||
|
private func todayString() -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.calendar = Calendar.current
|
||||||
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
formatter.timeZone = TimeZone.current
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
return formatter.string(from: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func rollDayIfNeeded() -> Bool {
|
||||||
|
let today = todayString()
|
||||||
|
if Defaults[\.unlockCountDate] != today {
|
||||||
|
Defaults[\.unlockCountDate] = today
|
||||||
|
Defaults[\.unlockCountToday] = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Counting
|
||||||
|
|
||||||
|
private func recordUnlock(reason: String) {
|
||||||
|
let now = Date()
|
||||||
|
rollDayIfNeeded()
|
||||||
|
Defaults[\.unlockCountToday] = Defaults[\.unlockCountToday] + 1
|
||||||
|
Defaults[\.unlockCountTotal] = Defaults[\.unlockCountTotal] + 1
|
||||||
|
Defaults[\.unlockLastCountedAt] = now
|
||||||
|
Defaults[\.unlockWasLocked] = false
|
||||||
|
isSessionLocked = false
|
||||||
|
Defaults[\.unlockLastHeartbeatAt] = now
|
||||||
|
stopProtectedDataPolling()
|
||||||
|
notifyChange()
|
||||||
|
print("🔓 UnlockCount +\(reason) today=\(Defaults[\.unlockCountToday]) total=\(Defaults[\.unlockCountTotal])")
|
||||||
|
debugLog("recordUnlock reason=\(reason) today=\(Defaults[\.unlockCountToday]) total=\(Defaults[\.unlockCountTotal])")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markLocked() {
|
||||||
|
isSessionLocked = true
|
||||||
|
hasSeenLockInSession = true
|
||||||
|
Defaults[\.unlockWasLocked] = true
|
||||||
|
Defaults[\.unlockLastHeartbeatAt] = Date()
|
||||||
|
startProtectedDataPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func persistLifecycleHeartbeat() {
|
||||||
|
Defaults[\.unlockLastHeartbeatAt] = Date()
|
||||||
|
if !UIApplication.shared.isProtectedDataAvailable {
|
||||||
|
Defaults[\.unlockWasLocked] = true
|
||||||
|
isSessionLocked = true
|
||||||
|
hasSeenLockInSession = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func notifyChange() {
|
||||||
|
NotificationCenter.default.post(name: .unlockCountDidChange, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func debugLog(_ message: String) {
|
||||||
|
let line = "\(Date()) \(logPrefix) \(message)"
|
||||||
|
NSLog("%@", line)
|
||||||
|
|
||||||
|
var logs = UserDefaults.standard.stringArray(forKey: debugLogDefaultsKey) ?? []
|
||||||
|
logs.append(line)
|
||||||
|
if logs.count > 200 {
|
||||||
|
logs.removeFirst(logs.count - 200)
|
||||||
|
}
|
||||||
|
UserDefaults.standard.set(logs, forKey: debugLogDefaultsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dumpStoredDebugLogs() {
|
||||||
|
let logs = UserDefaults.standard.stringArray(forKey: debugLogDefaultsKey) ?? []
|
||||||
|
guard !logs.isEmpty else { return }
|
||||||
|
NSLog("%@ storedLogCount=%d", logPrefix, logs.count)
|
||||||
|
logs.suffix(50).forEach { NSLog("%@ stored %@", logPrefix, $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startProtectedDataPolling() {
|
||||||
|
guard protectedDataPollTimer == nil else { return }
|
||||||
|
guard Defaults[\.unlockWasLocked] || isSessionLocked else { return }
|
||||||
|
debugLog("startProtectedDataPolling")
|
||||||
|
|
||||||
|
let timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.pollProtectedDataAvailability()
|
||||||
|
}
|
||||||
|
protectedDataPollTimer = timer
|
||||||
|
RunLoop.main.add(timer, forMode: .common)
|
||||||
|
pollProtectedDataAvailability()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopProtectedDataPolling() {
|
||||||
|
guard protectedDataPollTimer != nil else { return }
|
||||||
|
protectedDataPollTimer?.invalidate()
|
||||||
|
protectedDataPollTimer = nil
|
||||||
|
debugLog("stopProtectedDataPolling")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pollProtectedDataAvailability() {
|
||||||
|
let available = UIApplication.shared.isProtectedDataAvailable
|
||||||
|
debugLog("protectedDataPoll protected=\(available) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)")
|
||||||
|
guard available else { return }
|
||||||
|
guard Defaults[\.unlockWasLocked] || isSessionLocked else {
|
||||||
|
stopProtectedDataPolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recordUnlock(reason: "protectedDataPoll")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对照 protectedData 纠正锁态(定位回调补强,避免挂起丢通知)
|
||||||
|
private func syncLockStateFromProtectedData(reason: String) {
|
||||||
|
if !UIApplication.shared.isProtectedDataAvailable {
|
||||||
|
markLocked()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if Defaults[\.unlockWasLocked] || isSessionLocked {
|
||||||
|
recordUnlock(reason: reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 定位事件短窗口:锁态同步 + 心跳 + FinishTask 断言
|
||||||
|
private func performLocationTick(reason: String) {
|
||||||
|
let app = UIApplication.shared
|
||||||
|
var taskId = UIBackgroundTaskIdentifier.invalid
|
||||||
|
taskId = app.beginBackgroundTask(withName: "unlock.locationTick") {
|
||||||
|
if taskId != .invalid {
|
||||||
|
app.endBackgroundTask(taskId)
|
||||||
|
taskId = .invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLockStateFromProtectedData(reason: reason)
|
||||||
|
Defaults[\.unlockLastHeartbeatAt] = Date()
|
||||||
|
|
||||||
|
if taskId != .invalid {
|
||||||
|
app.endBackgroundTask(taskId)
|
||||||
|
taskId = .invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DAS / 定位冷启动短窗口:等 locationd 接管前尽量不被秒挂
|
||||||
|
private func beginBootBackgroundTask() {
|
||||||
|
endBootBackgroundTask()
|
||||||
|
let app = UIApplication.shared
|
||||||
|
bootBackgroundTask = app.beginBackgroundTask(withName: "unlock.boot") { [weak self] in
|
||||||
|
self?.debugLog("bootTask expired")
|
||||||
|
self?.endBootBackgroundTask()
|
||||||
|
}
|
||||||
|
debugLog("bootTask begin id=\(bootBackgroundTask.rawValue)")
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 25) { [weak self] in
|
||||||
|
self?.endBootBackgroundTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endBootBackgroundTask() {
|
||||||
|
guard bootBackgroundTask != .invalid else { return }
|
||||||
|
let id = bootBackgroundTask
|
||||||
|
bootBackgroundTask = .invalid
|
||||||
|
UIApplication.shared.endBackgroundTask(id)
|
||||||
|
debugLog("bootTask end id=\(id.rawValue)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - protectedData / lifecycle
|
||||||
|
|
||||||
|
@objc private func protectedDataWillBecomeUnavailable() {
|
||||||
|
debugLog("protectedDataWillBecomeUnavailable appState=\(UIApplication.shared.applicationState.rawValue)")
|
||||||
|
markLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func protectedDataDidBecomeAvailable() {
|
||||||
|
debugLog("protectedDataDidBecomeAvailable appState=\(UIApplication.shared.applicationState.rawValue) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)")
|
||||||
|
guard hasSeenLockInSession || Defaults[\.unlockWasLocked] else { return }
|
||||||
|
guard isSessionLocked || Defaults[\.unlockWasLocked] else { return }
|
||||||
|
recordUnlock(reason: "protectedData")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appDidEnterBackground() {
|
||||||
|
persistLifecycleHeartbeat()
|
||||||
|
restartLocationMonitoringIfPossible(reassertContinuous: true)
|
||||||
|
forceRefreshGeofenceFromLastKnown(reason: "didEnterBackground")
|
||||||
|
debugLog("didEnterBackground auth=\(authorizationStatus().rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) regions=\(locationManager.monitoredRegions.count)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appWillTerminate() {
|
||||||
|
persistLifecycleHeartbeat()
|
||||||
|
forceRefreshGeofenceFromLastKnown(reason: "willTerminate")
|
||||||
|
debugLog("willTerminate protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked])")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appDidBecomeActive() {
|
||||||
|
debugLog("didBecomeActive protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)")
|
||||||
|
endBootBackgroundTask()
|
||||||
|
reconcileUnlockIfNeeded(reason: "becomeActiveAfterLock")
|
||||||
|
restartLocationMonitoringIfPossible(reassertContinuous: true)
|
||||||
|
Defaults[\.unlockLastHeartbeatAt] = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 杀进程前已记锁屏,解锁后冷启动/回前台时补计
|
||||||
|
private func reconcileUnlockIfNeeded(reason: String) {
|
||||||
|
guard UIApplication.shared.isProtectedDataAvailable else { return }
|
||||||
|
guard Defaults[\.unlockWasLocked] || isSessionLocked else { return }
|
||||||
|
recordUnlock(reason: reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Location wake
|
||||||
|
|
||||||
|
private func authorizationStatus() -> CLAuthorizationStatus {
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
return locationManager.authorizationStatus
|
||||||
|
}
|
||||||
|
return CLLocationManager.authorizationStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info.plist 的 UIBackgroundModes 必须是含 location 的数组,才可安全打开后台持续定位
|
||||||
|
private var isLocationBackgroundModeEnabled: Bool {
|
||||||
|
guard let modes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return modes.contains("location")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restartLocationMonitoringIfPossible(reassertContinuous: Bool = false) {
|
||||||
|
guard authorizationStatus() == .authorizedAlways else {
|
||||||
|
stopAllLocationMonitoring()
|
||||||
|
stopProtectedDataPolling()
|
||||||
|
debugLog("stopLocation auth=\(authorizationStatus().rawValue) stopVisits=true stopCoarse=true stopGeofence=true")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重要位置变化 / Visit / 围栏:杀进程后由系统拉起
|
||||||
|
locationManager.startMonitoringSignificantLocationChanges()
|
||||||
|
locationManager.startMonitoringVisits()
|
||||||
|
debugLog("startVisits auth=\(authorizationStatus().rawValue)")
|
||||||
|
|
||||||
|
if isLocationBackgroundModeEnabled {
|
||||||
|
reassertContinuousLocationUpdates(forceStopFirst: reassertContinuous)
|
||||||
|
} else {
|
||||||
|
if #available(iOS 9.0, *) {
|
||||||
|
locationManager.allowsBackgroundLocationUpdates = false
|
||||||
|
coarseLocationManager.allowsBackgroundLocationUpdates = false
|
||||||
|
}
|
||||||
|
locationManager.stopUpdatingLocation()
|
||||||
|
coarseLocationManager.stopUpdatingLocation()
|
||||||
|
print("UnlockCountManager: UIBackgroundModes 缺少 location,仅使用重要位置变化/围栏")
|
||||||
|
debugLog("startSignificantOnly auth=\(authorizationStatus().rawValue)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopAllLocationMonitoring() {
|
||||||
|
locationManager.stopUpdatingLocation()
|
||||||
|
locationManager.stopMonitoringSignificantLocationChanges()
|
||||||
|
locationManager.stopMonitoringVisits()
|
||||||
|
coarseLocationManager.stopUpdatingLocation()
|
||||||
|
stopGeofenceMonitoring()
|
||||||
|
if #available(iOS 9.0, *) {
|
||||||
|
locationManager.allowsBackgroundLocationUpdates = false
|
||||||
|
coarseLocationManager.allowsBackgroundLocationUpdates = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停再开持续定位,迫使 locationd 重新挂上 Location subscription(对齐 Lookus 拉起后行为)
|
||||||
|
private func reassertContinuousLocationUpdates(forceStopFirst: Bool) {
|
||||||
|
guard authorizationStatus() == .authorizedAlways, isLocationBackgroundModeEnabled else { return }
|
||||||
|
if forceStopFirst {
|
||||||
|
locationManager.stopUpdatingLocation()
|
||||||
|
coarseLocationManager.stopUpdatingLocation()
|
||||||
|
debugLog("reassertContinuous stopThenStart")
|
||||||
|
}
|
||||||
|
if #available(iOS 9.0, *) {
|
||||||
|
locationManager.allowsBackgroundLocationUpdates = true
|
||||||
|
coarseLocationManager.allowsBackgroundLocationUpdates = true
|
||||||
|
}
|
||||||
|
locationManager.startUpdatingLocation()
|
||||||
|
coarseLocationManager.startUpdatingLocation()
|
||||||
|
debugLog("startPreciseLocation accuracy=10 distanceFilter=none activity=otherNavigation auth=\(authorizationStatus().rawValue) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable)")
|
||||||
|
debugLog("startCoarseLocation accuracy=800 distanceFilter=100 activity=otherNavigation auth=\(authorizationStatus().rawValue) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopGeofenceMonitoring() {
|
||||||
|
for region in locationManager.monitoredRegions where region.identifier.hasPrefix(geofenceIdPrefix) {
|
||||||
|
locationManager.stopMonitoring(for: region)
|
||||||
|
}
|
||||||
|
lastGeofenceCenter = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 中心 + 东南西北共 5 个围栏中心点(偏移约 40m)
|
||||||
|
private func geofenceGridCenters(around coordinate: CLLocationCoordinate2D) -> [(id: String, coordinate: CLLocationCoordinate2D)] {
|
||||||
|
let earthRadius: CLLocationDistance = 6_378_137
|
||||||
|
let latRad = coordinate.latitude * .pi / 180
|
||||||
|
let dLat = (geofenceSatelliteOffset / earthRadius) * 180 / .pi
|
||||||
|
let dLon = (geofenceSatelliteOffset / (earthRadius * cos(latRad))) * 180 / .pi
|
||||||
|
return [
|
||||||
|
(geofenceCenterId, coordinate),
|
||||||
|
("\(geofenceIdPrefix)n", CLLocationCoordinate2D(latitude: coordinate.latitude + dLat, longitude: coordinate.longitude)),
|
||||||
|
("\(geofenceIdPrefix)s", CLLocationCoordinate2D(latitude: coordinate.latitude - dLat, longitude: coordinate.longitude)),
|
||||||
|
("\(geofenceIdPrefix)e", CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude + dLon)),
|
||||||
|
("\(geofenceIdPrefix)w", CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: coordinate.longitude - dLon))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rememberLocation(_ location: CLLocation) {
|
||||||
|
lastKnownLocation = location
|
||||||
|
Defaults[\.unlockLastLatitude] = location.coordinate.latitude
|
||||||
|
Defaults[\.unlockLastLongitude] = location.coordinate.longitude
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedLastKnownLocation() -> CLLocation? {
|
||||||
|
if let location = locationManager.location ?? lastKnownLocation {
|
||||||
|
return location
|
||||||
|
}
|
||||||
|
if let lat = Defaults[\.unlockLastLatitude], let lon = Defaults[\.unlockLastLongitude] {
|
||||||
|
return CLLocation(latitude: lat, longitude: lon)
|
||||||
|
}
|
||||||
|
if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] {
|
||||||
|
return CLLocation(latitude: lat, longitude: lon)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func forceRefreshGeofenceFromLastKnown(reason: String) {
|
||||||
|
guard let location = resolvedLastKnownLocation() else {
|
||||||
|
debugLog("geofenceSkip reason=\(reason) noLastLocation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debugLog("geofenceForce reason=\(reason)")
|
||||||
|
refreshGeofence(around: location, force: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshGeofence(around location: CLLocation, force: Bool = false) {
|
||||||
|
guard authorizationStatus() == .authorizedAlways else { return }
|
||||||
|
guard CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) else {
|
||||||
|
debugLog("geofence unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let coordinate = location.coordinate
|
||||||
|
if !force, let last = lastGeofenceCenter {
|
||||||
|
let lastLocation = CLLocation(latitude: last.latitude, longitude: last.longitude)
|
||||||
|
if lastLocation.distance(from: location) < geofenceRefreshDistance {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopGeofenceMonitoring()
|
||||||
|
|
||||||
|
let centers = geofenceGridCenters(around: coordinate)
|
||||||
|
for item in centers {
|
||||||
|
let region = CLCircularRegion(
|
||||||
|
center: item.coordinate,
|
||||||
|
radius: geofenceRadius,
|
||||||
|
identifier: item.id
|
||||||
|
)
|
||||||
|
region.notifyOnEntry = true
|
||||||
|
region.notifyOnExit = true
|
||||||
|
locationManager.startMonitoring(for: region)
|
||||||
|
}
|
||||||
|
lastGeofenceCenter = coordinate
|
||||||
|
let ours = locationManager.monitoredRegions.filter { $0.identifier.hasPrefix(geofenceIdPrefix) }.count
|
||||||
|
debugLog("geofenceRefresh lat=\(coordinate.latitude) lon=\(coordinate.longitude) radius=\(geofenceRadius) grid=\(centers.count) force=\(force) monitoredOurs=\(ours) monitoredTotal=\(locationManager.monitoredRegions.count)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DAS 预热 / 定位拉起 / 任意非前台冷启动:立刻重申持续定位并尝试恢复围栏
|
||||||
|
private func handleNonActiveLaunchIfNeeded(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
|
||||||
|
let launchedByLocation = launchOptions?[.location] != nil
|
||||||
|
let nonActive = UIApplication.shared.applicationState != .active
|
||||||
|
guard launchedByLocation || nonActive else { return }
|
||||||
|
|
||||||
|
let reason: String
|
||||||
|
if launchedByLocation {
|
||||||
|
reason = "locationLaunch"
|
||||||
|
} else if UIApplication.shared.applicationState == .background {
|
||||||
|
reason = "backgroundLaunch"
|
||||||
|
} else {
|
||||||
|
reason = "inactiveLaunch"
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("handleLaunch reason=\(reason) locationKey=\(launchedByLocation) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) bgMode=\(isLocationBackgroundModeEnabled)")
|
||||||
|
reassertContinuousLocationUpdates(forceStopFirst: true)
|
||||||
|
forceRefreshGeofenceFromLastKnown(reason: reason)
|
||||||
|
performLocationTick(reason: reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - CLLocationManagerDelegate
|
||||||
|
|
||||||
|
extension UnlockCountManager: CLLocationManagerDelegate {
|
||||||
|
|
||||||
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
restartLocationMonitoringIfPossible(reassertContinuous: true)
|
||||||
|
debugLog("authChangedNewAPI auth=\(authorizationStatus().rawValue)")
|
||||||
|
notifyChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
||||||
|
restartLocationMonitoringIfPossible(reassertContinuous: true)
|
||||||
|
debugLog("authChangedOldAPI status=\(status.rawValue)")
|
||||||
|
notifyChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
|
let source = manager === coarseLocationManager ? "coarse" : "precise"
|
||||||
|
debugLog("didUpdateLocations source=\(source) count=\(locations.count) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)")
|
||||||
|
performLocationTick(reason: "locationUpdate")
|
||||||
|
if let latest = locations.last {
|
||||||
|
rememberLocation(latest)
|
||||||
|
refreshGeofence(around: latest)
|
||||||
|
}
|
||||||
|
// locationd 已开始送点,可结束 DAS 预热 boot task
|
||||||
|
endBootBackgroundTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
|
||||||
|
debugLog("didVisit arrival=\(visit.arrivalDate) departure=\(visit.departureDate) appState=\(UIApplication.shared.applicationState.rawValue) protected=\(UIApplication.shared.isProtectedDataAvailable) wasLocked=\(Defaults[\.unlockWasLocked]) sessionLocked=\(isSessionLocked)")
|
||||||
|
beginBootBackgroundTask()
|
||||||
|
reassertContinuousLocationUpdates(forceStopFirst: true)
|
||||||
|
performLocationTick(reason: "visit")
|
||||||
|
if visit.coordinate.latitude != 0 || visit.coordinate.longitude != 0 {
|
||||||
|
let location = CLLocation(latitude: visit.coordinate.latitude, longitude: visit.coordinate.longitude)
|
||||||
|
rememberLocation(location)
|
||||||
|
refreshGeofence(around: location, force: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
|
||||||
|
guard region.identifier.hasPrefix(geofenceIdPrefix) else { return }
|
||||||
|
debugLog("didEnterRegion id=\(region.identifier) appState=\(UIApplication.shared.applicationState.rawValue)")
|
||||||
|
beginBootBackgroundTask()
|
||||||
|
reassertContinuousLocationUpdates(forceStopFirst: true)
|
||||||
|
performLocationTick(reason: "geofenceEnter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
|
||||||
|
guard region.identifier.hasPrefix(geofenceIdPrefix) else { return }
|
||||||
|
debugLog("didExitRegion id=\(region.identifier) appState=\(UIApplication.shared.applicationState.rawValue)")
|
||||||
|
beginBootBackgroundTask()
|
||||||
|
reassertContinuousLocationUpdates(forceStopFirst: true)
|
||||||
|
performLocationTick(reason: "geofenceExit")
|
||||||
|
if let location = manager.location {
|
||||||
|
rememberLocation(location)
|
||||||
|
refreshGeofence(around: location, force: true)
|
||||||
|
} else {
|
||||||
|
forceRefreshGeofenceFromLastKnown(reason: "geofenceExit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
|
||||||
|
debugLog("geofenceStarted id=\(region.identifier)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
|
||||||
|
debugLog("geofenceFail id=\(region?.identifier ?? "nil") error=\(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
print("UnlockCountManager location error: \(error.localizedDescription)")
|
||||||
|
debugLog("locationError=\(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,496 @@
|
||||||
|
//
|
||||||
|
// GroupItineraryView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
import SwiftDate
|
||||||
|
|
||||||
|
/// 圈子 Tab 下的「行程」内容页
|
||||||
|
final class GroupItineraryView: UIView {
|
||||||
|
|
||||||
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
var onSwitchGroup: (() -> Void)?
|
||||||
|
var onDeleteSchedule: ((ScheduleModel) -> Void)?
|
||||||
|
var onSelectSchedule: ((ScheduleModel) -> Void)?
|
||||||
|
|
||||||
|
func updateGroupName(_ name: String) {
|
||||||
|
groupNameLab.text = name.isEmpty ? " " : name
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadMembers(_ members: [GroupMemberModel], selectedId: String, groupKey: String = "") {
|
||||||
|
memberList = Self.sortedMemberList(members, groupKey: groupKey)
|
||||||
|
// 保留有效选中;无效则用传入 selectedId → 自己 → 第一项(对齐首页)
|
||||||
|
if memberList.contains(where: { $0.user_id == selectedMemberId }) {
|
||||||
|
// keep selectedMemberId
|
||||||
|
} else if memberList.contains(where: { $0.user_id == selectedId }) {
|
||||||
|
selectedMemberId = selectedId
|
||||||
|
} else if let me = memberList.first(where: { $0.user_id == AppContextManager.shared.userId }) {
|
||||||
|
selectedMemberId = me.user_id
|
||||||
|
} else {
|
||||||
|
selectedMemberId = memberList.first?.user_id ?? ""
|
||||||
|
}
|
||||||
|
memberCV.reloadData()
|
||||||
|
scrollToSelectedMember(animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadSchedules(_ list: [ScheduleModel]) {
|
||||||
|
scheduleSections = Self.groupByDay(list)
|
||||||
|
tableView.reloadData()
|
||||||
|
emptyLab.isHidden = !list.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private var memberList: [GroupMemberModel] = []
|
||||||
|
private var selectedMemberId: String = ""
|
||||||
|
private var scheduleSections: [(title: String, items: [ScheduleModel])] = []
|
||||||
|
|
||||||
|
/// 与 HomeViewModel / GroupMemberView2 一致:圈主 -> 自己 -> 在线;同优先级保持接口原始顺序
|
||||||
|
private static func sortedMemberList(_ list: [GroupMemberModel], groupKey: String) -> [GroupMemberModel] {
|
||||||
|
list.enumerated()
|
||||||
|
.sorted { lhs, rhs in
|
||||||
|
let lhsPriority = memberSortPriority(lhs.element, groupKey: groupKey)
|
||||||
|
let rhsPriority = memberSortPriority(rhs.element, groupKey: groupKey)
|
||||||
|
if lhsPriority != rhsPriority { return lhsPriority < rhsPriority }
|
||||||
|
return lhs.offset < rhs.offset
|
||||||
|
}
|
||||||
|
.map(\.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func memberSortPriority(_ member: GroupMemberModel, groupKey: String) -> Int {
|
||||||
|
if !groupKey.isEmpty, groupKey.contains(member.user_id) { return 0 }
|
||||||
|
if member.user_id == AppContextManager.shared.userId { return 1 }
|
||||||
|
if member.is_online { return 2 }
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scrollToSelectedMember(animated: Bool) {
|
||||||
|
guard !selectedMemberId.isEmpty,
|
||||||
|
let idx = memberList.firstIndex(where: { $0.user_id == selectedMemberId }) else { return }
|
||||||
|
let indexPath = IndexPath(item: idx, section: 0)
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self,
|
||||||
|
self.memberCV.numberOfSections > 0,
|
||||||
|
self.memberCV.numberOfItems(inSection: 0) > idx else { return }
|
||||||
|
self.memberCV.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
addSubview(headerRow)
|
||||||
|
headerRow.addSubview(groupIcon)
|
||||||
|
headerRow.addSubview(groupNameLab)
|
||||||
|
headerRow.addSubview(changeGroupBtn)
|
||||||
|
|
||||||
|
let memberView = UIView()
|
||||||
|
memberView.backgroundColor = .white
|
||||||
|
memberView.cornerRadius = 20
|
||||||
|
memberView.addSubview(memberCV)
|
||||||
|
addSubview(memberView)
|
||||||
|
|
||||||
|
memberView.layoutChain
|
||||||
|
.topToBottomOfView(headerRow, offset: 12)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
.height(100)
|
||||||
|
|
||||||
|
addSubview(tableView)
|
||||||
|
addSubview(emptyLab)
|
||||||
|
|
||||||
|
headerRow.layoutChain
|
||||||
|
.top(8)
|
||||||
|
.edgesHorzontal(16)
|
||||||
|
.height(28)
|
||||||
|
|
||||||
|
groupIcon.layoutChain
|
||||||
|
.left()
|
||||||
|
.centerY()
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
|
||||||
|
changeGroupBtn.layoutChain
|
||||||
|
.right()
|
||||||
|
.centerY()
|
||||||
|
.height(28)
|
||||||
|
|
||||||
|
groupNameLab.layoutChain
|
||||||
|
.leftToRightOfView(groupIcon, offset: 8)
|
||||||
|
.centerY()
|
||||||
|
.rightToLeftOfView(changeGroupBtn, offset: -8, relation: .lessThanOrEqual)
|
||||||
|
|
||||||
|
memberCV.layoutChain
|
||||||
|
.edges(all: 5, excludingEdge: .bottom)
|
||||||
|
.bottom()
|
||||||
|
|
||||||
|
tableView.layoutChain
|
||||||
|
.topToBottomOfView(memberCV, offset: 8)
|
||||||
|
.edgesHorzontal()
|
||||||
|
.bottom()
|
||||||
|
|
||||||
|
emptyLab.layoutChain
|
||||||
|
.centerX()
|
||||||
|
.centerY(tableView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupRx() {
|
||||||
|
changeGroupBtn.rx.tap
|
||||||
|
.subscribe(onNext: { [weak self] in self?.onSwitchGroup?() })
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func groupByDay(_ list: [ScheduleModel]) -> [(title: String, items: [ScheduleModel])] {
|
||||||
|
let sorted = list.sorted { $0.timestamp > $1.timestamp }
|
||||||
|
var map: [String: [ScheduleModel]] = [:]
|
||||||
|
var order: [String] = []
|
||||||
|
let fmt = DateFormatter()
|
||||||
|
fmt.locale = Locale(identifier: "zh_CN")
|
||||||
|
let calendar = Calendar.current
|
||||||
|
for item in sorted {
|
||||||
|
let date = Date(timeIntervalSince1970: TimeInterval(item.timestamp) / 1000)
|
||||||
|
fmt.dateFormat = "M月d日"
|
||||||
|
let day = fmt.string(from: date)
|
||||||
|
let title: String
|
||||||
|
if calendar.isDateInToday(date) {
|
||||||
|
title = "今天 \(day)"
|
||||||
|
} else if calendar.isDateInYesterday(date) {
|
||||||
|
title = "昨天 \(day)"
|
||||||
|
} else {
|
||||||
|
fmt.dateFormat = "EEEE M月d日"
|
||||||
|
title = fmt.string(from: date)
|
||||||
|
}
|
||||||
|
if map[title] == nil {
|
||||||
|
order.append(title)
|
||||||
|
map[title] = []
|
||||||
|
}
|
||||||
|
map[title]?.append(item)
|
||||||
|
}
|
||||||
|
return order.map { ($0, map[$0] ?? []) }
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var headerRow: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .clear
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupIcon: UIImageView = {
|
||||||
|
let iv = UIImageView(image: UIImage(named: "Home/group_name_icon"))
|
||||||
|
iv.contentMode = .scaleAspectFit
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupNameLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||||
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
|
label.text = " "
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var changeGroupBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setTitle("切换圈子 ", for: .normal)
|
||||||
|
btn.setTitleColor(UIColor(hexStr: "#353B4F"), for: .normal)
|
||||||
|
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
btn.setImage(UIImage(named: "Common/arrow_right"), for: .normal)
|
||||||
|
btn.semanticContentAttribute = .forceRightToLeft
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var memberCV: UICollectionView = {
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.itemSize = CGSize(width: 61, height: 95)
|
||||||
|
layout.minimumLineSpacing = 15
|
||||||
|
layout.sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
cv.backgroundColor = .clear
|
||||||
|
cv.showsHorizontalScrollIndicator = false
|
||||||
|
cv.register(GroupMemberListCell.self)
|
||||||
|
cv.dataSource = self
|
||||||
|
cv.delegate = self
|
||||||
|
return cv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var tableView: UITableView = {
|
||||||
|
let tv = UITableView(frame: .zero, style: .grouped)
|
||||||
|
tv.backgroundColor = .clear
|
||||||
|
tv.separatorStyle = .none
|
||||||
|
tv.showsVerticalScrollIndicator = false
|
||||||
|
tv.register(GroupItineraryCell.self)
|
||||||
|
tv.dataSource = self
|
||||||
|
tv.delegate = self
|
||||||
|
tv.estimatedRowHeight = 100
|
||||||
|
tv.rowHeight = UITableView.automaticDimension
|
||||||
|
tv.sectionHeaderHeight = 36
|
||||||
|
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
tv.sectionHeaderTopPadding = 0
|
||||||
|
}
|
||||||
|
return tv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var emptyLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "暂无行程"
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#999999")
|
||||||
|
label.isHidden = true
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
|
setupUI()
|
||||||
|
setupRx()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UICollectionView
|
||||||
|
extension GroupItineraryView: UICollectionViewDataSource, UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
memberList.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||||
|
let model = memberList[indexPath.item]
|
||||||
|
cell.configure(
|
||||||
|
model: model,
|
||||||
|
isCurrentUser: model.user_id == AppContextManager.shared.userId,
|
||||||
|
isSelected: model.user_id == selectedMemberId
|
||||||
|
)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
selectedMemberId = memberList[indexPath.item].user_id
|
||||||
|
memberCV.reloadData()
|
||||||
|
scrollToSelectedMember(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableView
|
||||||
|
extension GroupItineraryView: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
scheduleSections.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
scheduleSections[section].items.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||||
|
let header = UIView()
|
||||||
|
header.backgroundColor = .clear
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = scheduleSections[section].title
|
||||||
|
label.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||||
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
|
header.addSubview(label)
|
||||||
|
label.layoutChain.left(16).centerY()
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||||
|
.leastNormalMagnitude
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||||
|
UIView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell: GroupItineraryCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(scheduleSections[indexPath.section].items[indexPath.row])
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
onSelectSchedule?(scheduleSections[indexPath.section].items[indexPath.row])
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView,
|
||||||
|
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
let model = scheduleSections[indexPath.section].items[indexPath.row]
|
||||||
|
let delete = UIContextualAction(style: .destructive, title: nil) { [weak self] _, _, done in
|
||||||
|
self?.onDeleteSchedule?(model)
|
||||||
|
done(true)
|
||||||
|
}
|
||||||
|
delete.image = UIImage(systemName: "trash")
|
||||||
|
delete.backgroundColor = UIColor(hexStr: "#FF5A5F")
|
||||||
|
return UISwipeActionsConfiguration(actions: [delete])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - GroupItineraryCell
|
||||||
|
final class GroupItineraryCell: UITableViewCell {
|
||||||
|
|
||||||
|
func configure(_ model: ScheduleModel) {
|
||||||
|
avatarImg.image = model.userIcon
|
||||||
|
nameLab.text = model.nick_name
|
||||||
|
let count = max(model.points.count, 0)
|
||||||
|
placeCountLab.text = "\(count)"
|
||||||
|
let date = Date(timeIntervalSince1970: TimeInterval(model.timestamp) / 1000)
|
||||||
|
let ongoing = Calendar.current.isDateInToday(date) || date > Date()
|
||||||
|
statusLab.text = ongoing ? "进行中" : "已完成"
|
||||||
|
statusLab.textColor = ongoing ? UIColor(hexStr: "#16B3FF") : UIColor(hexStr: "#999999")
|
||||||
|
statusLab.backgroundColor = ongoing
|
||||||
|
? UIColor(hexStr: "#E3F6FF")
|
||||||
|
: UIColor(hexStr: "#F0F0F0")
|
||||||
|
|
||||||
|
let tag = model.groups.first?.group_name ?? ""
|
||||||
|
tagLab.text = tag.isEmpty ? nil : " \(tag) "
|
||||||
|
tagLab.isHidden = tag.isEmpty
|
||||||
|
setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = .clear
|
||||||
|
contentView.backgroundColor = .clear
|
||||||
|
|
||||||
|
contentView.addSubview(cardView)
|
||||||
|
cardView.addSubview(avatarImg)
|
||||||
|
cardView.addSubview(nameLab)
|
||||||
|
cardView.addSubview(statusLab)
|
||||||
|
cardView.addSubview(summaryLab)
|
||||||
|
cardView.addSubview(placeCountLab)
|
||||||
|
cardView.addSubview(summaryTailLab)
|
||||||
|
cardView.addSubview(tagLab)
|
||||||
|
|
||||||
|
cardView.layoutChain
|
||||||
|
.edgesVertical(6)
|
||||||
|
.edgesHorzontal(16)
|
||||||
|
|
||||||
|
avatarImg.layoutChain
|
||||||
|
.top(14)
|
||||||
|
.left(14)
|
||||||
|
.width(36)
|
||||||
|
.height(36)
|
||||||
|
|
||||||
|
nameLab.layoutChain
|
||||||
|
.leftToRightOfView(avatarImg, offset: 10)
|
||||||
|
.centerY(avatarImg)
|
||||||
|
|
||||||
|
statusLab.layoutChain
|
||||||
|
.right(14)
|
||||||
|
.centerY(avatarImg)
|
||||||
|
.height(22)
|
||||||
|
|
||||||
|
summaryLab.layoutChain
|
||||||
|
.topToBottomOfView(avatarImg, offset: 12)
|
||||||
|
.left(14)
|
||||||
|
|
||||||
|
placeCountLab.layoutChain
|
||||||
|
.leftToRightOfView(summaryLab, offset: 4)
|
||||||
|
.centerY(summaryLab)
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
|
||||||
|
summaryTailLab.layoutChain
|
||||||
|
.leftToRightOfView(placeCountLab, offset: 4)
|
||||||
|
.centerY(summaryLab)
|
||||||
|
|
||||||
|
tagLab.layoutChain
|
||||||
|
.topToBottomOfView(summaryLab, offset: 12)
|
||||||
|
.right(14)
|
||||||
|
.bottom(14)
|
||||||
|
.height(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var cardView: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .white
|
||||||
|
v.cornerRadius = 16
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var avatarImg: UIImageView = {
|
||||||
|
let iv = UIImageView()
|
||||||
|
iv.contentMode = .scaleAspectFill
|
||||||
|
iv.cornerRadius = 18
|
||||||
|
iv.clipsToBounds = true
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var nameLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||||
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var statusLab: PaddingLabel = {
|
||||||
|
let label = PaddingLabel()
|
||||||
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.cornerRadius = 6
|
||||||
|
label.clipsToBounds = true
|
||||||
|
label.insets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var summaryLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "今天停留了"
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var placeCountLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = FontManager.youSheBiaoTiHei(12)
|
||||||
|
label.textColor = .white
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.backgroundColor = UIColor(hexStr: "#353B4F")
|
||||||
|
label.cornerRadius = 11
|
||||||
|
label.clipsToBounds = true
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var summaryTailLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "个地方"
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var tagLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#FF8A3D")
|
||||||
|
label.backgroundColor = UIColor(hexStr: "#FFE8D6")
|
||||||
|
label.cornerRadius = 4
|
||||||
|
label.clipsToBounds = true
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PaddingLabel: UILabel {
|
||||||
|
var insets = UIEdgeInsets.zero
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
super.drawText(in: rect.inset(by: insets))
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let size = super.intrinsicContentSize
|
||||||
|
return CGSize(width: size.width + insets.left + insets.right,
|
||||||
|
height: size.height + insets.top + insets.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -650,12 +650,16 @@ class DateCell: UICollectionViewCell {
|
||||||
// MARK: - GroupMemberListCell 成员
|
// MARK: - GroupMemberListCell 成员
|
||||||
class GroupMemberListCell: UICollectionViewCell {
|
class GroupMemberListCell: UICollectionViewCell {
|
||||||
|
|
||||||
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) {
|
private static let sosBlinkKey = "sosBlink"
|
||||||
|
|
||||||
|
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool, isSOSBlinking: Bool = false) {
|
||||||
avaterImgView.image = model.userIcon
|
avaterImgView.image = model.userIcon
|
||||||
vipIcon.image = model.vipIcon
|
// vipIcon.image = model.vipIcon
|
||||||
|
vipBgImgView.isHidden = model.level == 1
|
||||||
nameLab.text = model.nick_name
|
nameLab.text = model.nick_name
|
||||||
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
|
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
|
||||||
selectedBgView.isHidden = !isSelected
|
selectedBgView.isHidden = !isSelected
|
||||||
|
updateSOSBlink(isSOSBlinking)
|
||||||
// 会员权益
|
// 会员权益
|
||||||
// if AppContextManager.shared.vip > 1, model.is_online {
|
// if AppContextManager.shared.vip > 1, model.is_online {
|
||||||
// batteryInfoView.isHidden = model.battery.int == 0
|
// batteryInfoView.isHidden = model.battery.int == 0
|
||||||
|
|
@ -666,10 +670,36 @@ class GroupMemberListCell: UICollectionViewCell {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateSOSBlink(_ blinking: Bool) {
|
||||||
|
sosImgView.layer.removeAnimation(forKey: Self.sosBlinkKey)
|
||||||
|
if blinking {
|
||||||
|
sosImgView.isHidden = false
|
||||||
|
sosImgView.alpha = 1
|
||||||
|
let anim = CABasicAnimation(keyPath: "opacity")
|
||||||
|
anim.fromValue = 1.0
|
||||||
|
anim.toValue = 0.2
|
||||||
|
anim.duration = 0.6
|
||||||
|
anim.autoreverses = true
|
||||||
|
anim.repeatCount = .infinity
|
||||||
|
anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
sosImgView.layer.add(anim, forKey: Self.sosBlinkKey)
|
||||||
|
} else {
|
||||||
|
sosImgView.isHidden = true
|
||||||
|
sosImgView.alpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
updateSOSBlink(false)
|
||||||
|
}
|
||||||
|
|
||||||
private func setupSubviews() {
|
private func setupSubviews() {
|
||||||
contentView.addSubview(selectedBgView)
|
contentView.addSubview(selectedBgView)
|
||||||
contentView.addSubview(avaterImgView)
|
contentView.addSubview(avaterImgView)
|
||||||
contentView.addSubview(vipIcon)
|
contentView.addSubview(sosImgView)
|
||||||
|
contentView.addSubview(vipBgImgView)
|
||||||
|
// contentView.addSubview(vipIcon)
|
||||||
contentView.addSubview(batteryInfoView)
|
contentView.addSubview(batteryInfoView)
|
||||||
batteryInfoView.addSubview(cornerView)
|
batteryInfoView.addSubview(cornerView)
|
||||||
cornerView.addSubview(batteryView)
|
cornerView.addSubview(batteryView)
|
||||||
|
|
@ -688,6 +718,16 @@ class GroupMemberListCell: UICollectionViewCell {
|
||||||
.edgesHorzontal(5.5)
|
.edgesHorzontal(5.5)
|
||||||
.heightToWidth(1)
|
.heightToWidth(1)
|
||||||
|
|
||||||
|
sosImgView.layoutChain
|
||||||
|
.topToView(avaterImgView)
|
||||||
|
.leftToView(avaterImgView)
|
||||||
|
.rightToView(avaterImgView)
|
||||||
|
.bottomToView(avaterImgView)
|
||||||
|
|
||||||
|
vipBgImgView.layoutChain
|
||||||
|
.top(7)
|
||||||
|
.centerX()
|
||||||
|
|
||||||
batteryInfoView.layoutChain
|
batteryInfoView.layoutChain
|
||||||
.leftToView(avaterImgView)
|
.leftToView(avaterImgView)
|
||||||
.rightToView(avaterImgView)
|
.rightToView(avaterImgView)
|
||||||
|
|
@ -712,14 +752,14 @@ class GroupMemberListCell: UICollectionViewCell {
|
||||||
.right(5)
|
.right(5)
|
||||||
.centerY()
|
.centerY()
|
||||||
|
|
||||||
vipIcon.layoutChain
|
// vipIcon.layoutChain
|
||||||
.topToView(avaterImgView, offset: -8)
|
// .topToView(avaterImgView, offset: -8)
|
||||||
.leftToView(avaterImgView, offset: -6)
|
// .leftToView(avaterImgView, offset: -6)
|
||||||
.width(25)
|
// .width(25)
|
||||||
.height(21)
|
// .height(21)
|
||||||
|
|
||||||
nameLab.layoutChain
|
nameLab.layoutChain
|
||||||
.topToBottomOfView(batteryInfoView, offset: 4)
|
.topToBottomOfView(batteryInfoView, offset: 8)
|
||||||
.edgesHorzontal()
|
.edgesHorzontal()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -732,6 +772,13 @@ class GroupMemberListCell: UICollectionViewCell {
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
lazy var vipBgImgView: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.image = UIImage(named: "GroupMemberList/vip")
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var avaterImgView: UIImageView = {
|
lazy var avaterImgView: UIImageView = {
|
||||||
let view = UIImageView()
|
let view = UIImageView()
|
||||||
view.backgroundColor = .lightGray
|
view.backgroundColor = .lightGray
|
||||||
|
|
@ -740,6 +787,13 @@ class GroupMemberListCell: UICollectionViewCell {
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
lazy var sosImgView: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.image = UIImage(named: "GroupMemberList/sos")
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var batteryInfoView: UIView = {
|
lazy var batteryInfoView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = .clear
|
view.backgroundColor = .clear
|
||||||
|
|
|
||||||
|
|
@ -24,133 +24,228 @@ class GroupView: UIView {
|
||||||
|
|
||||||
var disposeBag = DisposeBag()
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
/// 0 = 圈子,1 = 行程
|
||||||
|
private(set) var currentTopTab: Int = 0
|
||||||
|
|
||||||
/// 当前是否吸顶
|
/// 当前是否吸顶
|
||||||
private var isSticky = false
|
private var isSticky = false
|
||||||
/// 内部 tableview 是否可滚动
|
/// 内部 tableview 是否可滚动
|
||||||
private var isSubCanScroll = false
|
private var isSubCanScroll = false
|
||||||
|
|
||||||
var stickThreshold: CGFloat {
|
var stickThreshold: CGFloat {
|
||||||
return segmentView.dl.y
|
// segment 在 listContainer 内,吸顶以 listContainer 顶部为准
|
||||||
|
return listContainer.dl.y
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
addSubview(navBgView)
|
addSubview(navBgView)
|
||||||
addSubview(navView)
|
addSubview(topBar)
|
||||||
navView.addRightButton(scanBtn)
|
topBar.addSubview(circleTabLabel)
|
||||||
|
topBar.addSubview(itineraryTabLabel)
|
||||||
|
topBar.addSubview(messageBtn)
|
||||||
|
|
||||||
addSubview(mainScrollView)
|
addSubview(circlePage)
|
||||||
|
circlePage.addSubview(mainScrollView)
|
||||||
mainScrollView.addSubview(contentView)
|
mainScrollView.addSubview(contentView)
|
||||||
contentView.addSubview(cycleScrollView)
|
|
||||||
contentView.addSubview(actionButtonsView)
|
contentView.addSubview(actionButtonsView)
|
||||||
actionButtonsView.addSubview(createGroupBtn)
|
actionButtonsView.addSubview(createGroupBtn)
|
||||||
actionButtonsView.addSubview(joinGroupBtn)
|
actionButtonsView.addSubview(joinGroupBtn)
|
||||||
|
contentView.addSubview(listContainer)
|
||||||
|
listContainer.addSubview(segmentView)
|
||||||
|
segmentView.addSubview(joinedTabLabel)
|
||||||
|
segmentView.addSubview(createdTabLabel)
|
||||||
|
segmentView.addSubview(tabIndicator)
|
||||||
|
listContainer.addSubview(segmentScrollView)
|
||||||
|
segmentScrollView.addSubview(segmentContentView)
|
||||||
|
// 全部圈子(joined) | 我的圈子(created)
|
||||||
|
segmentContentView.addSubview(joinedTableView)
|
||||||
|
segmentContentView.addSubview(createdTableView)
|
||||||
|
|
||||||
|
// 保留绑定入口,设计无轮播/热门时隐藏
|
||||||
|
contentView.addSubview(cycleScrollView)
|
||||||
contentView.addSubview(hotGroupTitleLabel)
|
contentView.addSubview(hotGroupTitleLabel)
|
||||||
contentView.addSubview(hotGroupsCollectionView)
|
contentView.addSubview(hotGroupsCollectionView)
|
||||||
contentView.addSubview(segmentView)
|
cycleScrollView.isHidden = true
|
||||||
segmentView.addSubview(createdTabLabel)
|
hotGroupTitleLabel.isHidden = true
|
||||||
segmentView.addSubview(joinedTabLabel)
|
hotGroupsCollectionView.isHidden = true
|
||||||
segmentView.addSubview(tabIndicator)
|
|
||||||
contentView.addSubview(segmentScrollView)
|
addSubview(itineraryPage)
|
||||||
segmentScrollView.addSubview(segmentContentView)
|
itineraryPage.isHidden = true
|
||||||
segmentContentView.addSubview(createdTableView)
|
|
||||||
segmentContentView.addSubview(joinedTableView)
|
|
||||||
|
|
||||||
navBgView.layoutChain
|
navBgView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
.height(kNaviHeight)
|
.heightToWidth(160/375)
|
||||||
|
|
||||||
navView.layoutChain
|
topBar.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
.height(kNaviHeight)
|
.height(kNaviHeight)
|
||||||
|
|
||||||
mainScrollView.layoutChain
|
circleTabLabel.layoutChain
|
||||||
.topToBottomOfView(navBgView)
|
.left(20)
|
||||||
|
.bottom(10)
|
||||||
|
.height(28)
|
||||||
|
|
||||||
|
itineraryTabLabel.layoutChain
|
||||||
|
.leftToRightOfView(circleTabLabel, offset: 20)
|
||||||
|
.centerY(circleTabLabel)
|
||||||
|
.height(28)
|
||||||
|
|
||||||
|
messageBtn.layoutChain
|
||||||
|
.right(16)
|
||||||
|
.centerY(circleTabLabel)
|
||||||
|
.width(28)
|
||||||
|
.height(28)
|
||||||
|
|
||||||
|
circlePage.layoutChain
|
||||||
|
.topToBottomOfView(topBar)
|
||||||
.edges(excludingEdge: .top)
|
.edges(excludingEdge: .top)
|
||||||
|
|
||||||
|
itineraryPage.layoutChain
|
||||||
|
.topToBottomOfView(topBar)
|
||||||
|
.edges(excludingEdge: .top)
|
||||||
|
|
||||||
|
mainScrollView.layoutChain
|
||||||
|
.edges()
|
||||||
|
|
||||||
contentView.layoutChain
|
contentView.layoutChain
|
||||||
.edges()
|
.edges()
|
||||||
.widthToView(mainScrollView)
|
.widthToView(mainScrollView)
|
||||||
|
|
||||||
// 轮播图
|
|
||||||
cycleScrollView.layoutChain
|
|
||||||
.top(10)
|
|
||||||
.left(15).right(15)
|
|
||||||
.heightToWidth(100/375)
|
|
||||||
|
|
||||||
// 创建/加入圈子
|
|
||||||
actionButtonsView.layoutChain
|
actionButtonsView.layoutChain
|
||||||
.topToBottomOfView(cycleScrollView, offset: 38)
|
.top(12)
|
||||||
.edgesHorzontal()
|
.edgesHorzontal(16)
|
||||||
|
.height(108)
|
||||||
|
|
||||||
createGroupBtn.layoutChain
|
createGroupBtn.layoutChain
|
||||||
.left(15)
|
.left()
|
||||||
.edgesVertical()
|
.edgesVertical()
|
||||||
.widthToView(joinGroupBtn)
|
.widthToView(joinGroupBtn)
|
||||||
.height(40)
|
|
||||||
|
|
||||||
joinGroupBtn.layoutChain
|
joinGroupBtn.layoutChain
|
||||||
.topToView(createGroupBtn)
|
.topToView(createGroupBtn)
|
||||||
.bottomToView(createGroupBtn)
|
.bottomToView(createGroupBtn)
|
||||||
.leftToRightOfView(createGroupBtn, offset: 21)
|
.leftToRightOfView(createGroupBtn, offset: 12)
|
||||||
.right(15)
|
.right()
|
||||||
.widthToView(joinGroupBtn)
|
.widthToView(createGroupBtn)
|
||||||
|
|
||||||
// 热门酷圈
|
listContainer.layoutChain
|
||||||
hotGroupTitleLabel.layoutChain
|
.topToBottomOfView(actionButtonsView, offset: 16)
|
||||||
.topToBottomOfView(actionButtonsView, offset: 21)
|
|
||||||
.left(16)
|
|
||||||
|
|
||||||
hotGroupsCollectionView.layoutChain
|
|
||||||
.topToBottomOfView(hotGroupTitleLabel, offset: 8)
|
|
||||||
.edgesHorzontal()
|
.edgesHorzontal()
|
||||||
.height(88)
|
.bottom()
|
||||||
|
|
||||||
// Segment
|
|
||||||
let labelWidth = (UIScreen.main.bounds.width - 60) / 2
|
let labelWidth = (UIScreen.main.bounds.width - 60) / 2
|
||||||
segmentView.layoutChain
|
segmentView.layoutChain
|
||||||
.topToBottomOfView(hotGroupsCollectionView)
|
.top()
|
||||||
.edgesHorzontal()
|
.edgesHorzontal()
|
||||||
.height(44)
|
.height(44)
|
||||||
|
|
||||||
createdTabLabel.layoutChain
|
joinedTabLabel.layoutChain
|
||||||
.left(30).centerY()
|
.left(30).centerY()
|
||||||
.width(labelWidth)
|
.width(labelWidth)
|
||||||
|
|
||||||
joinedTabLabel.layoutChain
|
createdTabLabel.layoutChain
|
||||||
.right(30).centerY()
|
.right(30).centerY()
|
||||||
.width(labelWidth)
|
.width(labelWidth)
|
||||||
|
|
||||||
tabIndicator.layoutChain
|
tabIndicator.layoutChain
|
||||||
.bottom().centerX(createdTabLabel)
|
.topToBottomOfView(joinedTabLabel, offset: 4)
|
||||||
.width(14).height(3)
|
.centerX(joinedTabLabel)
|
||||||
|
.width(19).height(4)
|
||||||
|
|
||||||
// 横向 segmentScrollView
|
|
||||||
segmentScrollView.layoutChain
|
segmentScrollView.layoutChain
|
||||||
.topToBottomOfView(segmentView)
|
.topToBottomOfView(segmentView)
|
||||||
.edgesHorzontal()
|
.edgesHorzontal()
|
||||||
.bottom()
|
.bottom()
|
||||||
.height(kScreenHeight - kNaviHeight - 44)
|
.height(kScreenHeight - kNaviHeight - 44 - 12 - 108 - 16)
|
||||||
|
|
||||||
segmentContentView.layoutChain
|
segmentContentView.layoutChain
|
||||||
.edges()
|
.edges()
|
||||||
.heightToView(segmentScrollView)
|
.heightToView(segmentScrollView)
|
||||||
|
|
||||||
let svWidth = UIScreen.main.bounds.width
|
let svWidth = UIScreen.main.bounds.width
|
||||||
createdTableView.layoutChain
|
joinedTableView.layoutChain
|
||||||
.top().bottom().left()
|
.top().bottom().left()
|
||||||
.width(svWidth)
|
.width(svWidth)
|
||||||
|
|
||||||
joinedTableView.layoutChain
|
createdTableView.layoutChain
|
||||||
.top().bottom()
|
.top().bottom()
|
||||||
.leftToRightOfView(createdTableView)
|
.leftToRightOfView(joinedTableView)
|
||||||
.width(svWidth)
|
.width(svWidth)
|
||||||
|
|
||||||
|
// 离屏占位,避免影响布局
|
||||||
|
cycleScrollView.layoutChain
|
||||||
|
.top(0).left(0).width(0).height(0)
|
||||||
|
hotGroupTitleLabel.layoutChain
|
||||||
|
.top(0).left(0).width(0).height(0)
|
||||||
|
hotGroupsCollectionView.layoutChain
|
||||||
|
.top(0).left(0).width(0).height(0)
|
||||||
|
|
||||||
|
configureActionCard(
|
||||||
|
createGroupBtn,
|
||||||
|
bgColor: UIColor(hexStr: "#D6F3FF"),
|
||||||
|
title: "创建圈子",
|
||||||
|
subtitle: "和朋友一起定位玩"
|
||||||
|
)
|
||||||
|
configureActionCard(
|
||||||
|
joinGroupBtn,
|
||||||
|
bgColor: UIColor(hexStr: "#E8F8C8"),
|
||||||
|
title: "加入圈子",
|
||||||
|
subtitle: "输入邀请码快速加入"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureActionCard(_ card: UIView, bgColor: UIColor, title: String, subtitle: String) {
|
||||||
|
card.backgroundColor = bgColor
|
||||||
|
card.cornerRadius = 16
|
||||||
|
card.clipsToBounds = true
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = title
|
||||||
|
titleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
||||||
|
titleLab.textColor = UIColor(hexStr: "#0F2846")
|
||||||
|
|
||||||
|
let subLab = UILabel()
|
||||||
|
subLab.text = subtitle
|
||||||
|
subLab.font = .systemFont(ofSize: 11, weight: .medium)
|
||||||
|
subLab.textColor = UIColor(hexStr: "#6B7A90")
|
||||||
|
|
||||||
|
let cta = UILabel()
|
||||||
|
cta.text = "立即体验"
|
||||||
|
cta.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||||
|
cta.textColor = UIColor(hexStr: "#0F2846")
|
||||||
|
cta.textAlignment = .center
|
||||||
|
cta.backgroundColor = .white
|
||||||
|
cta.cornerRadius = 12
|
||||||
|
cta.clipsToBounds = true
|
||||||
|
|
||||||
|
card.addSubview(titleLab)
|
||||||
|
card.addSubview(subLab)
|
||||||
|
card.addSubview(cta)
|
||||||
|
|
||||||
|
titleLab.layoutChain
|
||||||
|
.top(16)
|
||||||
|
.left(14)
|
||||||
|
.right(14)
|
||||||
|
|
||||||
|
subLab.layoutChain
|
||||||
|
.topToBottomOfView(titleLab, offset: 6)
|
||||||
|
.left(14)
|
||||||
|
.right(14)
|
||||||
|
|
||||||
|
cta.layoutChain
|
||||||
|
.left(14)
|
||||||
|
.bottom(14)
|
||||||
|
.width(72)
|
||||||
|
.height(24)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
segmentContentView.layoutChain.width(bounds.width * 2)
|
segmentContentView.layoutChain.width(bounds.width * 2)
|
||||||
|
listContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
listContainer.layer.cornerRadius = 20
|
||||||
|
listContainer.clipsToBounds = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Nav
|
// MARK: - Nav
|
||||||
|
|
@ -161,9 +256,41 @@ class GroupView: UIView {
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// MARK: - Top Bar
|
||||||
|
lazy var topBar: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .clear
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var circleTabLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "圈子"
|
||||||
|
label.font = FontManager.youSheBiaoTiHei(32)
|
||||||
|
label.textColor = UIColor(hexStr: "#0F2846")
|
||||||
|
label.isUserInteractionEnabled = true
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var itineraryTabLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "行程"
|
||||||
|
label.font = FontManager.youSheBiaoTiHei(24)
|
||||||
|
label.textColor = UIColor(hexStr: "#0F2846").withAlphaComponent(0.45)
|
||||||
|
label.isUserInteractionEnabled = true
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var messageBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setImage(UIImage(named: "Home/message"), for: .normal)
|
||||||
|
btn.isHidden = true
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var navView: BaseNavigationView = {
|
lazy var navView: BaseNavigationView = {
|
||||||
let nav = BaseNavigationView(title: "圈子")
|
let nav = BaseNavigationView(title: "圈子")
|
||||||
nav.backBtn.isHidden = true
|
nav.isHidden = true
|
||||||
return nav
|
return nav
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -174,6 +301,18 @@ class GroupView: UIView {
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// MARK: - Pages
|
||||||
|
lazy var circlePage: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .clear
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var itineraryPage: GroupItineraryView = {
|
||||||
|
let v = GroupItineraryView()
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
// MARK: - Main Scroll
|
// MARK: - Main Scroll
|
||||||
lazy var mainScrollView: PanScrollView = {
|
lazy var mainScrollView: PanScrollView = {
|
||||||
let sv = PanScrollView()
|
let sv = PanScrollView()
|
||||||
|
|
@ -190,7 +329,7 @@ class GroupView: UIView {
|
||||||
return v
|
return v
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// MARK: - 轮播图
|
// MARK: - 轮播图(隐藏保留绑定)
|
||||||
lazy var cycleScrollView: SDCycleScrollView = {
|
lazy var cycleScrollView: SDCycleScrollView = {
|
||||||
let view = SDCycleScrollView(frame: .zero)
|
let view = SDCycleScrollView(frame: .zero)
|
||||||
view.backgroundColor = .lightGray
|
view.backgroundColor = .lightGray
|
||||||
|
|
@ -206,34 +345,26 @@ class GroupView: UIView {
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// MARK: - 创建 / 加入 圈子
|
// MARK: - 创建 / 加入 圈子(色卡,整卡可点)
|
||||||
private lazy var actionButtonsView: UIView = {
|
private lazy var actionButtonsView: UIView = {
|
||||||
let v = UIView()
|
let v = UIView()
|
||||||
v.backgroundColor = .clear
|
v.backgroundColor = .clear
|
||||||
return v
|
return v
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var createGroupBtn: UIButton = {
|
lazy var createGroupBtn: UIView = {
|
||||||
let btn = UIButton(type: .custom)
|
let v = UIView()
|
||||||
btn.setTitle("创建圈子", for: .normal)
|
v.isUserInteractionEnabled = true
|
||||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
return v
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
|
|
||||||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
|
||||||
btn.cornerRadius = 20
|
|
||||||
return btn
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var joinGroupBtn: UIButton = {
|
lazy var joinGroupBtn: UIView = {
|
||||||
let btn = UIButton(type: .custom)
|
let v = UIView()
|
||||||
btn.setTitle("加入圈子", for: .normal)
|
v.isUserInteractionEnabled = true
|
||||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
return v
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
|
|
||||||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
|
||||||
btn.cornerRadius = 20
|
|
||||||
return btn
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// MARK: - 热门酷圈
|
// MARK: - 热门酷圈(隐藏保留绑定)
|
||||||
private lazy var hotGroupTitleLabel: UILabel = {
|
private lazy var hotGroupTitleLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "热门酷圈"
|
label.text = "热门酷圈"
|
||||||
|
|
@ -255,37 +386,46 @@ class GroupView: UIView {
|
||||||
return cv
|
return cv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// MARK: - Segment
|
// MARK: - List container
|
||||||
|
private lazy var listContainer: UIView = {
|
||||||
|
let v = UIView()
|
||||||
|
v.backgroundColor = .white
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: - Segment(0 全部圈子=joined,1 我的圈子=created)
|
||||||
private(set) lazy var segmentView: UIView = {
|
private(set) lazy var segmentView: UIView = {
|
||||||
let v = UIView()
|
let v = UIView()
|
||||||
v.backgroundColor = .white
|
v.backgroundColor = .white
|
||||||
|
|
||||||
let line = UIView()
|
let line = UIView()
|
||||||
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
|
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
|
||||||
v.addSubview(line)
|
// v.addSubview(line)
|
||||||
line.layoutChain
|
// line.layoutChain
|
||||||
.edgesHorzontal()
|
// .edgesHorzontal()
|
||||||
.height(1)
|
// .height(1)
|
||||||
.bottom(4)
|
// .bottom(4)
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private(set) lazy var createdTabLabel: UILabel = {
|
/// 「全部圈子」→ joined 列表(index 0)
|
||||||
|
private(set) lazy var joinedTabLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "我创建的圈子"
|
label.text = "全部圈子"
|
||||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
label.font = .systemFont(ofSize: 16, weight: .heavy)
|
||||||
label.textColor = UIColor(hexStr: "#1A1A1A")
|
label.textColor = UIColor(hexStr: "#2A3648")
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
label.isUserInteractionEnabled = true
|
label.isUserInteractionEnabled = true
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private(set) lazy var joinedTabLabel: UILabel = {
|
/// 「我的圈子」→ created 列表(index 1)
|
||||||
|
private(set) lazy var createdTabLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "我加入的圈子"
|
label.text = "我的圈子"
|
||||||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||||
label.textColor = UIColor(hexStr: "#999999")
|
label.textColor = UIColor(hexStr: "#2A3648")
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
label.isUserInteractionEnabled = true
|
label.isUserInteractionEnabled = true
|
||||||
return label
|
return label
|
||||||
|
|
@ -293,7 +433,7 @@ class GroupView: UIView {
|
||||||
|
|
||||||
private lazy var tabIndicator: UIView = {
|
private lazy var tabIndicator: UIView = {
|
||||||
let v = UIView()
|
let v = UIView()
|
||||||
v.backgroundColor = UIColor(hexStr: "#16B3FF")
|
v.backgroundColor = UIColor(hexStr: "#85D8FF")
|
||||||
v.layer.cornerRadius = 2
|
v.layer.cornerRadius = 2
|
||||||
return v
|
return v
|
||||||
}()
|
}()
|
||||||
|
|
@ -342,25 +482,40 @@ class GroupView: UIView {
|
||||||
// MARK: - Segment
|
// MARK: - Segment
|
||||||
var currentSegmentIndex: Int = 0
|
var currentSegmentIndex: Int = 0
|
||||||
|
|
||||||
|
/// index: 0=全部圈子(joined),1=我的圈子(created)
|
||||||
func selectSegment(at index: Int) {
|
func selectSegment(at index: Int) {
|
||||||
currentSegmentIndex = index
|
currentSegmentIndex = index
|
||||||
let isCreated = index == 0
|
let isAll = index == 0
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.createdTabLabel.textColor = isCreated ? UIColor(hexStr: "#1A1A1A") : UIColor(hexStr: "#999999")
|
self.joinedTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648") : UIColor(hexStr: "#2A3648", alpha: 0.5)
|
||||||
self.joinedTabLabel.textColor = isCreated ? UIColor(hexStr: "#999999") : UIColor(hexStr: "#1A1A1A")
|
self.joinedTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .heavy : .medium)
|
||||||
|
self.createdTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#2A3648")
|
||||||
|
self.createdTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .medium : .heavy)
|
||||||
|
|
||||||
self.tabIndicator.center.x = isCreated
|
self.tabIndicator.center.x = isAll
|
||||||
? self.createdTabLabel.center.x
|
? self.joinedTabLabel.center.x
|
||||||
: self.joinedTabLabel.center.x
|
: self.createdTabLabel.center.x
|
||||||
|
|
||||||
self.layoutIfNeeded()
|
self.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectTopTab(at index: Int) {
|
||||||
|
guard index == 0 || index == 1 else { return }
|
||||||
|
currentTopTab = index
|
||||||
|
let isCircle = index == 0
|
||||||
|
circlePage.isHidden = !isCircle
|
||||||
|
itineraryPage.isHidden = isCircle
|
||||||
|
circleTabLabel.font = FontManager.youSheBiaoTiHei(isCircle ? 32 : 24)
|
||||||
|
itineraryTabLabel.font = FontManager.youSheBiaoTiHei(isCircle ? 24 : 32)
|
||||||
|
circleTabLabel.textColor = isCircle ? UIColor(hexStr: "#353B4F") : UIColor(hexStr: "#2A3648", alpha: 0.5)
|
||||||
|
itineraryTabLabel.textColor = isCircle ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#353B4F")
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
backgroundColor = .white
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
setupUI()
|
setupUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -586,11 +741,6 @@ final class CircleGroupCell: UITableViewCell {
|
||||||
|
|
||||||
func configure(_ model: GroupCellData) {
|
func configure(_ model: GroupCellData) {
|
||||||
let g = model.groupInfo
|
let g = model.groupInfo
|
||||||
// if let faceURL = g.faceURL, !faceURL.isEmpty {
|
|
||||||
// iconView.dl.setImage(with: faceURL)
|
|
||||||
// } else {
|
|
||||||
// iconView.image = UIImage(named: "GroupIcon1")
|
|
||||||
// }
|
|
||||||
iconView.image = model.localIcon
|
iconView.image = model.localIcon
|
||||||
nameLabel.text = g.groupName ?? ""
|
nameLabel.text = g.groupName ?? ""
|
||||||
countLabel.text = "共\(g.memberCount)个成员"
|
countLabel.text = "共\(g.memberCount)个成员"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import RxCocoa
|
||||||
import RxDataSources
|
import RxDataSources
|
||||||
import SDCycleScrollView
|
import SDCycleScrollView
|
||||||
import OpenIMSDK
|
import OpenIMSDK
|
||||||
|
import ObjectMapper
|
||||||
|
|
||||||
final class GroupViewController: BaseViewController {
|
final class GroupViewController: BaseViewController {
|
||||||
|
|
||||||
|
|
@ -17,6 +18,10 @@ final class GroupViewController: BaseViewController {
|
||||||
fileprivate var rootView: GroupView!
|
fileprivate var rootView: GroupView!
|
||||||
private let viewModel = GroupViewModel()
|
private let viewModel = GroupViewModel()
|
||||||
|
|
||||||
|
/// 行程页当前圈子数据
|
||||||
|
private var itineraryGroupModel: GroupModel?
|
||||||
|
private var itinerarySchedules: [ScheduleModel] = []
|
||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
rootView = GroupView(frame: UIScreen.main.bounds)
|
rootView = GroupView(frame: UIScreen.main.bounds)
|
||||||
view = rootView
|
view = rootView
|
||||||
|
|
@ -27,6 +32,7 @@ final class GroupViewController: BaseViewController {
|
||||||
bindViewModel()
|
bindViewModel()
|
||||||
reactiveAction()
|
reactiveAction()
|
||||||
observeTableViews()
|
observeTableViews()
|
||||||
|
bindItineraryPage()
|
||||||
requestRecommandGroup()
|
requestRecommandGroup()
|
||||||
requestGroupInfo()
|
requestGroupInfo()
|
||||||
|
|
||||||
|
|
@ -123,14 +129,30 @@ final class GroupViewController: BaseViewController {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.createdTabLabel.rx.tapGesture
|
// 子 Tab:全部圈子(0=joined) / 我的圈子(1=created)
|
||||||
|
rootView.joinedTabLabel.rx.tapGesture
|
||||||
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })
|
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
rootView.joinedTabLabel.rx.tapGesture
|
rootView.createdTabLabel.rx.tapGesture
|
||||||
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(1) })
|
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(1) })
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
// 顶栏:圈子 / 行程
|
||||||
|
rootView.circleTabLabel.rx.tapGesture
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
self?.rootView.selectTopTab(at: 0)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.itineraryTabLabel.rx.tapGesture
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.rootView.selectTopTab(at: 1)
|
||||||
|
self.refreshItineraryPage()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
Observable.merge(
|
Observable.merge(
|
||||||
rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(),
|
rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(),
|
||||||
rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable()
|
rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable()
|
||||||
|
|
@ -159,12 +181,90 @@ final class GroupViewController: BaseViewController {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func bindItineraryPage() {
|
||||||
|
let page = rootView.itineraryPage
|
||||||
|
|
||||||
|
page.onSwitchGroup = { [weak self] in
|
||||||
|
self?.showSwitchGroupPop()
|
||||||
|
}
|
||||||
|
|
||||||
|
page.onSelectSchedule = { model in
|
||||||
|
AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()])
|
||||||
|
}
|
||||||
|
|
||||||
|
page.onDeleteSchedule = { [weak self] model in
|
||||||
|
self?.deleteSchedule(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func switchToSegment(_ index: Int) {
|
private func switchToSegment(_ index: Int) {
|
||||||
rootView.selectSegment(at: index)
|
rootView.selectSegment(at: index)
|
||||||
let offset = CGPoint(x: CGFloat(index) * rootView.segmentScrollView.bounds.width, y: 0)
|
let offset = CGPoint(x: CGFloat(index) * rootView.segmentScrollView.bounds.width, y: 0)
|
||||||
rootView.segmentScrollView.setContentOffset(offset, animated: false)
|
rootView.segmentScrollView.setContentOffset(offset, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 行程页
|
||||||
|
private func refreshItineraryPage() {
|
||||||
|
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self = self, let model = response.model else { return }
|
||||||
|
self.itineraryGroupModel = model
|
||||||
|
let current = model.groups.first(where: { $0.group_key == model.default_group_key })
|
||||||
|
self.rootView.itineraryPage.updateGroupName(current?.name ?? "")
|
||||||
|
self.rootView.itineraryPage.reloadMembers(
|
||||||
|
model.select_group_employee,
|
||||||
|
selectedId: AppContextManager.shared.userId,
|
||||||
|
groupKey: model.default_group_key
|
||||||
|
)
|
||||||
|
self.requestItinerarySchedules(groupKey: model.default_group_key)
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestItinerarySchedules(groupKey: String) {
|
||||||
|
guard !groupKey.isEmpty else {
|
||||||
|
itinerarySchedules = []
|
||||||
|
rootView.itineraryPage.reloadSchedules([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ItineraryService.groupScheduleList(groupKey: groupKey)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.itinerarySchedules = response.list
|
||||||
|
self.rootView.itineraryPage.reloadSchedules(response.list)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showSwitchGroupPop() {
|
||||||
|
guard let groupModel = itineraryGroupModel else {
|
||||||
|
refreshItineraryPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in
|
||||||
|
guard let self = self, let key = groupKey else { return }
|
||||||
|
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
self?.refreshItineraryPage()
|
||||||
|
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
|
||||||
|
})
|
||||||
|
.disposed(by: self.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteSchedule(_ model: ScheduleModel) {
|
||||||
|
guard !model.id.isEmpty else { return }
|
||||||
|
DLToast.showLoading()
|
||||||
|
ItineraryService.delete(id: model.id)
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
DLToast.dismiss()
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.itinerarySchedules.removeAll { $0.id == model.id }
|
||||||
|
self.rootView.itineraryPage.reloadSchedules(self.itinerarySchedules)
|
||||||
|
}, onError: { _ in
|
||||||
|
DLToast.dismiss()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 内层 tableView 滚动
|
// MARK: - 内层 tableView 滚动
|
||||||
private func observeTableViews() {
|
private func observeTableViews() {
|
||||||
rootView.createdTableView.rx.didScroll
|
rootView.createdTableView.rx.didScroll
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,9 @@ final class GroupViewModel {
|
||||||
return m
|
return m
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 「全部圈子」= 全部已加入群;「我的圈子」= 我创建的
|
||||||
|
var all: [GroupCellData] = []
|
||||||
var created: [GroupCellData] = []
|
var created: [GroupCellData] = []
|
||||||
var joined: [GroupCellData] = []
|
|
||||||
|
|
||||||
for g in cachedGroups {
|
for g in cachedGroups {
|
||||||
let conv = convMap[g.groupID ?? ""]
|
let conv = convMap[g.groupID ?? ""]
|
||||||
|
|
@ -97,15 +98,14 @@ final class GroupViewModel {
|
||||||
lastMsgTime: Int(conv?.latestMsgSendTime ?? 0),
|
lastMsgTime: Int(conv?.latestMsgSendTime ?? 0),
|
||||||
localIcon: getLocalIcon(groupId: g.groupID ?? "")
|
localIcon: getLocalIcon(groupId: g.groupID ?? "")
|
||||||
)
|
)
|
||||||
|
all.append(data)
|
||||||
if g.ownerUserID == currentUserId || g.creatorUserID == currentUserId {
|
if g.ownerUserID == currentUserId || g.creatorUserID == currentUserId {
|
||||||
created.append(data)
|
created.append(data)
|
||||||
} else {
|
|
||||||
joined.append(data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createdSectionsSubject.onNext(created.mapSection())
|
createdSectionsSubject.onNext(created.mapSection())
|
||||||
joinedSectionsSubject.onNext(joined.mapSection())
|
joinedSectionsSubject.onNext(all.mapSection())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import ObjectMapper
|
||||||
import RxSwift
|
import RxSwift
|
||||||
import RxCocoa
|
import RxCocoa
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
import Photos
|
||||||
|
|
||||||
class InviteJoinVC: BaseViewController {
|
class InviteJoinVC: BaseViewController {
|
||||||
|
|
||||||
|
|
@ -25,16 +26,24 @@ class InviteJoinVC: BaseViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
// Do any additional setup after loading the view.
|
// 分享邀请卡片到微信
|
||||||
|
rootView.shareBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||||
// 分享
|
self?.shareInviteCard()
|
||||||
rootView.shareAppBtn.rx.tap.subscribe(onNext: { _ in
|
|
||||||
SharePopView.show()
|
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
// 分享邀请码
|
// 下载图片到相册
|
||||||
rootView.shareCodeBtn.rx.tap.subscribe(onNext: { [weak self] in
|
rootView.downloadBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||||
guard let self = self else { return }
|
self?.downloadInviteCard()
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
|
guard let model = groupInModel else { return }
|
||||||
|
rootView.configure(model: model)
|
||||||
|
requestOperateGroup(key: model.group_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Share / Download
|
||||||
|
|
||||||
|
private func shareInviteCard() {
|
||||||
guard let config = AppContextManager.shared.systemConfig,
|
guard let config = AppContextManager.shared.systemConfig,
|
||||||
let shareConfig = config.shareConfig else { return }
|
let shareConfig = config.shareConfig else { return }
|
||||||
|
|
||||||
|
|
@ -50,7 +59,7 @@ class InviteJoinVC: BaseViewController {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
|
|
||||||
guard let screenshot = self.rootView.inviteInfoView.takeScreenshot(),
|
guard let screenshot = self.rootView.inviteCardView.takeScreenshot(),
|
||||||
let imageData = screenshot.pngData() else {
|
let imageData = screenshot.pngData() else {
|
||||||
DLToast.showError(text: "分享失败")
|
DLToast.showError(text: "分享失败")
|
||||||
return
|
return
|
||||||
|
|
@ -87,12 +96,56 @@ class InviteJoinVC: BaseViewController {
|
||||||
} else {
|
} else {
|
||||||
sendMessage(fallbackImage)
|
sendMessage(fallbackImage)
|
||||||
}
|
}
|
||||||
}).disposed(by: disposeBag)
|
}
|
||||||
|
|
||||||
guard let model = groupInModel else { return }
|
private func downloadInviteCard() {
|
||||||
rootView.groupIcon.image = model.groupIcon
|
guard let screenshot = rootView.inviteCardView.takeScreenshot() else {
|
||||||
rootView.groupNameLab.text = model.name
|
DLToast.showError(text: "保存失败")
|
||||||
requestOperateGroup(key: model.group_key)
|
return
|
||||||
|
}
|
||||||
|
let save = { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
UIImageWriteToSavedPhotosAlbum(
|
||||||
|
screenshot, self,
|
||||||
|
#selector(self.image(_:didFinishSavingWithError:contextInfo:)),
|
||||||
|
nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if #available(iOS 14, *) {
|
||||||
|
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if status == .authorized || status == .limited {
|
||||||
|
save()
|
||||||
|
} else {
|
||||||
|
Permission.openAppSetting(
|
||||||
|
title: "无法访问相册",
|
||||||
|
message: "请在设置中允许访问相册以保存邀请图片"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Permission.photosAuthorizationStatus { granted in
|
||||||
|
if granted {
|
||||||
|
save()
|
||||||
|
} else {
|
||||||
|
Permission.openAppSetting(
|
||||||
|
title: "无法访问相册",
|
||||||
|
message: "请在设置中允许访问相册以保存邀请图片"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func image(_ image: UIImage,
|
||||||
|
didFinishSavingWithError error: Error?,
|
||||||
|
contextInfo: UnsafeRawPointer) {
|
||||||
|
if error != nil {
|
||||||
|
DLToast.showError(text: "保存失败")
|
||||||
|
} else {
|
||||||
|
DLToast.showSuccess(text: "已保存")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - API
|
// MARK: - API
|
||||||
|
|
@ -103,8 +156,8 @@ class InviteJoinVC: BaseViewController {
|
||||||
let data = response.data,
|
let data = response.data,
|
||||||
let code = data["share_code"] as? String else { return }
|
let code = data["share_code"] as? String else { return }
|
||||||
self.rootView.inviteCodeLab.text = code
|
self.rootView.inviteCodeLab.text = code
|
||||||
self.rootView.qrCodeImgView.image = code.createQRCode(size: self.rootView.qrCodeImgView.dl.size)
|
self.rootView.qrCodeImgView.image = code.createQRCode(size: CGSize(width: 120, height: 120))
|
||||||
}, onError: { (error) in
|
}, onError: { _ in
|
||||||
DLToast.show(text: "获取错误,请重试") {
|
DLToast.show(text: "获取错误,请重试") {
|
||||||
AppRouter.shared.popOrDismiss()
|
AppRouter.shared.popOrDismiss()
|
||||||
}
|
}
|
||||||
|
|
@ -120,5 +173,4 @@ class InviteJoinVC: BaseViewController {
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,154 +13,220 @@ class InviteJoinView: UIView {
|
||||||
|
|
||||||
var disposeBag = DisposeBag()
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
private func setupRx() {
|
func configure(model: GroupInfoModel) {
|
||||||
|
groupIcon.image = model.groupIcon
|
||||||
|
groupNameLab.text = model.name
|
||||||
|
groupDescLab.text = model.description
|
||||||
|
groupDescLab.isHidden = model.description.isEmpty
|
||||||
|
|
||||||
inviteCodeView.rx.tapGesture.subscribe { _ in
|
tagStack.arrangedSubviews.forEach {
|
||||||
guard let code = self.inviteCodeLab.text else { return }
|
tagStack.removeArrangedSubview($0)
|
||||||
|
$0.removeFromSuperview()
|
||||||
|
}
|
||||||
|
let labels = Array(model.labels.prefix(2))
|
||||||
|
tagStack.isHidden = labels.isEmpty
|
||||||
|
for text in labels {
|
||||||
|
tagStack.addArrangedSubview(Self.makeTagLabel(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeTagLabel(_ text: String) -> UILabel {
|
||||||
|
let label = InviteTagLabel()
|
||||||
|
label.text = text
|
||||||
|
label.textColor = UIColor(hexStr: "#D5923F")
|
||||||
|
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||||||
|
label.backgroundColor = UIColor(hexStr: "#FFF9DD")
|
||||||
|
label.cornerRadius = 4
|
||||||
|
label.clipsToBounds = true
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupRx() {
|
||||||
|
inviteCodeView.rx.tapGesture.subscribe { [weak self] _ in
|
||||||
|
guard let code = self?.inviteCodeLab.text, !code.isEmpty else { return }
|
||||||
UIPasteboard.general.string = code
|
UIPasteboard.general.string = code
|
||||||
DLToast.showSuccess(text: "已复制")
|
DLToast.showSuccess(text: "已复制")
|
||||||
}.disposed(by: disposeBag)
|
}.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
addSubview(navBgView)
|
|
||||||
addSubview(navView)
|
addSubview(navView)
|
||||||
addSubview(groupIcon)
|
addSubview(inviteCardView)
|
||||||
addSubview(groupNameLab)
|
inviteCardView.addSubview(cardBgImg)
|
||||||
addSubview(inviteInfoView)
|
inviteCardView.addSubview(groupView)
|
||||||
inviteInfoView.addSubview(inviteInfoTitleLab)
|
inviteCardView.addSubview(inviteInfoTitleLab)
|
||||||
inviteInfoView.addSubview(inviteTipsLab)
|
inviteCardView.addSubview(inviteCodeView)
|
||||||
inviteInfoView.addSubview(inviteCodeView)
|
|
||||||
inviteCodeView.addSubview(inviteCodeTipsLab)
|
inviteCodeView.addSubview(inviteCodeTipsLab)
|
||||||
inviteCodeView.addSubview(inviteCodeLab)
|
inviteCodeView.addSubview(inviteCodeLab)
|
||||||
inviteInfoView.addSubview(scanTipsLab)
|
inviteCardView.addSubview(inviteTipsLab)
|
||||||
inviteInfoView.addSubview(qrCodeView)
|
inviteCardView.addSubview(scanTipsLab)
|
||||||
qrCodeView.addSubview(qrCodeBgImg)
|
inviteCardView.addSubview(qrCodeView)
|
||||||
qrCodeView.addSubview(qrCodeImgView)
|
qrCodeView.addSubview(qrCodeImgView)
|
||||||
addSubview(shareAppBtn)
|
addSubview(shareBtn)
|
||||||
addSubview(shareCodeBtn)
|
addSubview(downloadBtn)
|
||||||
|
|
||||||
navBgView.layoutChain
|
|
||||||
.edges(excludingEdge: .bottom)
|
|
||||||
.heightToWidth(160/375)
|
|
||||||
|
|
||||||
navView.layoutChain
|
navView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
.height(kNaviHeight)
|
.height(kNaviHeight)
|
||||||
|
|
||||||
groupIcon.layoutChain
|
shareBtn.layoutChain
|
||||||
.topToBottomOfView(navView, offset: 24)
|
.left(30)
|
||||||
.width(80)
|
.bottom(kSafeBottomMargin + 12)
|
||||||
.height(80)
|
.height(56)
|
||||||
.centerX()
|
.width(110)
|
||||||
|
|
||||||
groupNameLab.layoutChain
|
downloadBtn.layoutChain
|
||||||
.topToBottomOfView(groupIcon, offset: 4)
|
.leftToRightOfView(shareBtn, offset: 16)
|
||||||
.centerX()
|
.right(30)
|
||||||
|
.bottomToView(shareBtn)
|
||||||
|
.heightToView(shareBtn)
|
||||||
|
|
||||||
shareAppBtn.layoutChain
|
inviteCardView.layoutChain
|
||||||
.left(44)
|
.topToBottomOfView(navView, offset: 42)
|
||||||
.bottom(kSafeBottomMargin + 10)
|
.edgesHorzontal(15)
|
||||||
.widthToView(shareCodeBtn)
|
.heightToWidth(481/345)
|
||||||
.height(50)
|
|
||||||
|
|
||||||
shareCodeBtn.layoutChain
|
cardBgImg.layoutChain.edges()
|
||||||
.leftToRightOfView(shareAppBtn, offset: 20)
|
|
||||||
.right(44)
|
|
||||||
.bottomToView(shareAppBtn)
|
|
||||||
.widthToView(shareAppBtn)
|
|
||||||
.heightToView(shareAppBtn)
|
|
||||||
|
|
||||||
inviteInfoView.layoutChain
|
groupView.layoutChain
|
||||||
.topToBottomOfView(groupIcon, offset: 50)
|
.top(16)
|
||||||
.edgesHorzontal(30)
|
.edgesHorzontal(16)
|
||||||
// .bottomToTopOfView(shareAppBtn, offset: -32)
|
|
||||||
|
|
||||||
inviteInfoTitleLab.layoutChain
|
inviteInfoTitleLab.layoutChain
|
||||||
.top(20)
|
.topToBottomOfView(groupIcon, offset: 28)
|
||||||
.centerX()
|
|
||||||
|
|
||||||
inviteTipsLab.layoutChain
|
|
||||||
.topToBottomOfView(inviteInfoTitleLab, offset: 9)
|
|
||||||
.centerX()
|
.centerX()
|
||||||
|
|
||||||
inviteCodeView.layoutChain
|
inviteCodeView.layoutChain
|
||||||
.topToBottomOfView(inviteTipsLab, offset: 10)
|
.topToBottomOfView(inviteInfoTitleLab, offset: 12)
|
||||||
.centerX()
|
.centerX()
|
||||||
.width(180)
|
.width(200)
|
||||||
.height(50)
|
.height(56)
|
||||||
|
|
||||||
inviteCodeTipsLab.layoutChain
|
inviteCodeTipsLab.layoutChain
|
||||||
.top(10)
|
.top(8)
|
||||||
.centerX()
|
.centerX()
|
||||||
|
|
||||||
inviteCodeLab.layoutChain
|
inviteCodeLab.layoutChain
|
||||||
.topToBottomOfView(inviteCodeTipsLab)
|
.topToBottomOfView(inviteCodeTipsLab, offset: 7)
|
||||||
.centerX()
|
.centerX()
|
||||||
|
|
||||||
scanTipsLab.layoutChain
|
inviteTipsLab.layoutChain
|
||||||
.topToBottomOfView(inviteCodeView, offset: 20)
|
.topToBottomOfView(inviteCodeView, offset: 10)
|
||||||
.centerX()
|
.centerX()
|
||||||
|
|
||||||
qrCodeView.layoutChain
|
qrCodeView.layoutChain
|
||||||
.topToBottomOfView(scanTipsLab, offset: 15)
|
.bottom(42)
|
||||||
.centerX()
|
.centerX()
|
||||||
.widthToHeight(1)
|
.width(138)
|
||||||
.bottom(20)
|
.height(138)
|
||||||
|
|
||||||
qrCodeBgImg.layoutChain.edges()
|
|
||||||
|
|
||||||
qrCodeImgView.layoutChain
|
qrCodeImgView.layoutChain
|
||||||
.edgesHorzontal(4)
|
.edges(all: 18)
|
||||||
.edgesVertical(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy var navBgView: UIImageView = {
|
scanTipsLab.layoutChain
|
||||||
let iv = UIImageView()
|
.bottomToTopOfView(qrCodeView, offset: -8)
|
||||||
iv.image = UIImage(named: "Common/navBar_bg_2")
|
.centerX()
|
||||||
iv.contentMode = .scaleAspectFill
|
}
|
||||||
return iv
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var navView: BaseNavigationView = {
|
lazy var navView: BaseNavigationView = {
|
||||||
let nav = BaseNavigationView(title: "邀请成员")
|
let nav = BaseNavigationView(title: "邀请成员")
|
||||||
|
nav.backBtn.setImage(UIImage(named: "Group/invite_back"), for: .normal)
|
||||||
|
nav.backBtn.layoutChain.width(36).height(36)
|
||||||
return nav
|
return nav
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
/// 整卡(截图 / 分享用)
|
||||||
|
lazy var inviteCardView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var cardBgImg: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.image = UIImage(named: "Group/invite_card_bg")
|
||||||
|
view.contentMode = .scaleAspectFit
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var groupView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#EEF7FB")
|
||||||
|
view.cornerRadius = 18
|
||||||
|
|
||||||
|
view.addSubview(groupIcon)
|
||||||
|
groupIcon.layoutChain
|
||||||
|
.left(12)
|
||||||
|
.edgesVertical(9)
|
||||||
|
.width(52)
|
||||||
|
.heightToWidth(1)
|
||||||
|
|
||||||
|
view.addSubview(groupNameLab)
|
||||||
|
groupNameLab.layoutChain
|
||||||
|
.topToView(groupIcon, offset: 2)
|
||||||
|
.leftToRightOfView(groupIcon, offset: 10)
|
||||||
|
|
||||||
|
view.addSubview(tagStack)
|
||||||
|
tagStack.layoutChain
|
||||||
|
.leftToRightOfView(groupNameLab, offset: 6)
|
||||||
|
.centerY(groupNameLab)
|
||||||
|
.right(12, relation: .greaterThanOrEqual)
|
||||||
|
|
||||||
|
view.addSubview(groupDescLab)
|
||||||
|
groupDescLab.layoutChain
|
||||||
|
.topToBottomOfView(groupNameLab, offset: 14)
|
||||||
|
.leftToView(groupNameLab)
|
||||||
|
.right(24)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var groupIcon: UIImageView = {
|
lazy var groupIcon: UIImageView = {
|
||||||
let view = UIImageView()
|
let view = UIImageView()
|
||||||
view.backgroundColor = .clear
|
view.contentMode = .scaleAspectFill
|
||||||
view.cornerRadius = 40
|
view.cornerRadius = 10
|
||||||
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var groupNameLab: UILabel = {
|
lazy var groupNameLab: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
label.textColor = UIColor(hexStr: "#293445")
|
||||||
|
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var inviteInfoView: UIView = {
|
lazy var tagStack: UIStackView = {
|
||||||
let view = UIView()
|
let view = UIStackView()
|
||||||
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
|
view.axis = .horizontal
|
||||||
view.cornerRadius = 20
|
view.alignment = .center
|
||||||
|
view.spacing = 4
|
||||||
|
view.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
lazy var groupDescLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||||
|
label.textColor = UIColor(hexStr: "#999999")
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var inviteInfoTitleLab: UILabel = {
|
lazy var inviteInfoTitleLab: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "将此邀请码分享给加入圈子的人"
|
label.text = "将此邀请码分享给加入圈子的人"
|
||||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var inviteTipsLab: UILabel = {
|
lazy var inviteTipsLab: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "此邀请码有效期为24小时"
|
label.text = "此邀请码有效期为24小时"
|
||||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||||
label.textColor = UIColor(hexStr: "#16B3FF")
|
label.textColor = UIColor(hexStr: "#00ADFE")
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -169,78 +235,80 @@ class InviteJoinView: UIView {
|
||||||
view.borderColor = UIColor(hexStr: "#CCCCCC")
|
view.borderColor = UIColor(hexStr: "#CCCCCC")
|
||||||
view.borderWidth = 1
|
view.borderWidth = 1
|
||||||
view.cornerRadius = 10
|
view.cornerRadius = 10
|
||||||
|
view.backgroundColor = .white
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var inviteCodeTipsLab: UILabel = {
|
lazy var inviteCodeTipsLab: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "点击复制邀请码"
|
label.text = "点击复制邀请码"
|
||||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
||||||
label.textColor = ThemeManager.shared.color.contentColor
|
label.textColor = UIColor(hexStr: "#999999")
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var inviteCodeLab: UILabel = {
|
lazy var inviteCodeLab: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 16, weight: .heavy)
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
label.textColor = UIColor(hexStr: "#00ADFE")
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var scanTipsLab: UILabel = {
|
lazy var scanTipsLab: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "— 或扫描以下二维码 —"
|
label.text = "扫描以下二维码"
|
||||||
label.font = .systemFont(ofSize: 10)
|
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||||
label.textColor = ThemeManager.shared.color.contentColor
|
label.textColor = ThemeManager.shared.color.contentColor
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var qrCodeView: UIView = {
|
lazy var qrCodeView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = .clear
|
view.backgroundColor = .white
|
||||||
return view
|
view.borderWidth = 3
|
||||||
}()
|
view.borderColor = UIColor(hexStr: "#EAEEEF")
|
||||||
|
view.cornerRadius = 34
|
||||||
lazy var qrCodeBgImg: UIImageView = {
|
|
||||||
let view = UIImageView()
|
|
||||||
view.image = UIImage(named: "Group/scan_bg")
|
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var qrCodeImgView: UIImageView = {
|
lazy var qrCodeImgView: UIImageView = {
|
||||||
let view = UIImageView()
|
let view = UIImageView()
|
||||||
view.backgroundColor = .clear
|
view.backgroundColor = .white
|
||||||
view.contentMode = .scaleAspectFill
|
view.contentMode = .scaleAspectFit
|
||||||
view.cornerRadius = 10
|
view.cornerRadius = 8
|
||||||
view.clipsToBounds = true
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var shareAppBtn: UIButton = {
|
lazy var shareBtn: UIButton = {
|
||||||
let btn = UIButton(type: .custom)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setTitle("分享APP", for: .normal)
|
btn.setTitle("分享", for: .normal)
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||||
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
|
btn.setTitleColor(UIColor(hexStr: "#00ADFE"), for: .normal)
|
||||||
btn.backgroundColor = .white
|
btn.backgroundColor = .white
|
||||||
btn.borderWidth = 1
|
btn.borderWidth = 1
|
||||||
btn.borderColor = UIColor(hexStr: "#16B3FF")
|
btn.borderColor = UIColor(hexStr: "#00ADFE")
|
||||||
btn.cornerRadius = 25
|
btn.cornerRadius = 20
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var shareCodeBtn: UIButton = {
|
lazy var downloadBtn: UIButton = {
|
||||||
let btn = UIButton(type: .custom)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setTitle("分享邀请码", for: .normal)
|
btn.setTitle("下载图片", for: .normal)
|
||||||
btn.setTitleColor(.white, for: .normal)
|
btn.setTitleColor(.white, for: .normal)
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||||
btn.cornerRadius = 25
|
btn.cornerRadius = 20
|
||||||
|
btn.clipsToBounds = true
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
/// 兼容旧引用:截图目标
|
||||||
|
var inviteInfoView: UIView { inviteCardView }
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
backgroundColor = .white
|
backgroundColor = .clear
|
||||||
setupUI()
|
setupUI()
|
||||||
setupRx()
|
setupRx()
|
||||||
}
|
}
|
||||||
|
|
@ -248,4 +316,29 @@ class InviteJoinView: UIView {
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
setGradientLayer(
|
||||||
|
frame: bounds,
|
||||||
|
startPoint: CGPoint(x: 0.5, y: 0),
|
||||||
|
endPoint: CGPoint(x: 0.5, y: 1),
|
||||||
|
colors: [UIColor(hexStr: "#A1E8FF"), UIColor(hexStr: "#E4F8FF")],
|
||||||
|
locations: [0, 1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InviteTagLabel: UILabel {
|
||||||
|
private let inset = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
super.drawText(in: rect.inset(by: inset))
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let size = super.intrinsicContentSize
|
||||||
|
return CGSize(width: size.width + inset.left + inset.right,
|
||||||
|
height: size.height + inset.top + inset.bottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,23 @@ class GroupListPopView: UIView {
|
||||||
|
|
||||||
private static let shared = GroupListPopView(frame: CGRect(origin: .zero, size: kScreenSize))
|
private static let shared = GroupListPopView(frame: CGRect(origin: .zero, size: kScreenSize))
|
||||||
|
|
||||||
|
private static let rowHeight: CGFloat = 72
|
||||||
|
private static let maxVisibleRows = 3
|
||||||
|
private static let primaryBlue = UIColor(hexStr: "#16B3FF")
|
||||||
|
private static let cardBg = UIColor(hexStr: "#F5FBFF")
|
||||||
|
private static let tagBg = UIColor(hexStr: "#FFF9DD")
|
||||||
|
private static let tagText = UIColor(hexStr: "#D5923F")
|
||||||
|
|
||||||
private var groupModel: GroupModel? {
|
private var groupModel: GroupModel? {
|
||||||
didSet {
|
didSet {
|
||||||
guard let model = groupModel else { return }
|
guard let model = groupModel else { return }
|
||||||
groupNameLab.text = model.groups.first(where: { $0.group_key == model.default_group_key })?.name ?? ""
|
selectedGroupKey = model.default_group_key
|
||||||
defaultGroupKey = model.default_group_key
|
|
||||||
groupList = model.groups
|
groupList = model.groups
|
||||||
|
updateTableHeight()
|
||||||
let viewHeight = CGFloat(model.groups.prefix(5).count * 68)
|
|
||||||
tableView.layoutChain.height(viewHeight, relation: .greaterThanOrEqual)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var defaultGroupKey: String = ""
|
private var selectedGroupKey: String = ""
|
||||||
|
|
||||||
private var groupList: [GroupInfoModel] = [] {
|
private var groupList: [GroupInfoModel] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
|
|
@ -34,18 +38,16 @@ class GroupListPopView: UIView {
|
||||||
/// 完成选中进行回调
|
/// 完成选中进行回调
|
||||||
private var completion: ((String?) -> Void)?
|
private var completion: ((String?) -> Void)?
|
||||||
|
|
||||||
@objc func tap() {
|
@objc private func tapMask() {
|
||||||
completion?(nil)
|
completion?(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func createAction(button: UIButton) {
|
@objc private func cancelAction() {
|
||||||
completion?(nil)
|
completion?(nil)
|
||||||
AppRouter.push(Route.createGroup)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func joinAction(button: UIButton) {
|
@objc private func confirmAction() {
|
||||||
completion?(nil)
|
completion?(selectedGroupKey.isEmpty ? nil : selectedGroupKey)
|
||||||
AppRouter.push(Route.joinGroup)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var bgView: UIView = {
|
private lazy var bgView: UIView = {
|
||||||
|
|
@ -55,79 +57,63 @@ class GroupListPopView: UIView {
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 圈子选择
|
|
||||||
lazy var groupView: UIView = {
|
|
||||||
let view = UIView()
|
|
||||||
view.backgroundColor = .white
|
|
||||||
view.cornerRadius = 18
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var groupIconView: UIImageView = {
|
|
||||||
let view = UIImageView()
|
|
||||||
view.image = UIImage(named: "Home/group")
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
view.contentMode = .scaleAspectFill
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var groupNameLab: UILabel = {
|
|
||||||
let label = UILabel()
|
|
||||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
|
||||||
label.textColor = UIColor(hexStr: "#0F2846")
|
|
||||||
label.textAlignment = .center
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var groupArrowIconView: UIImageView = {
|
|
||||||
let view = UIImageView()
|
|
||||||
view.image = UIImage(named: "Home/arrow_down")
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
view.contentMode = .scaleAspectFill
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var infoView: UIView = {
|
lazy var infoView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = .white
|
view.backgroundColor = .clear
|
||||||
view.cornerRadius = 10
|
|
||||||
|
let bg = UIView()
|
||||||
|
bg.backgroundColor = UIColor(hexStr: "#FDFDFD")
|
||||||
|
view.addSubview(bg)
|
||||||
|
bg.layoutChain
|
||||||
|
.top(80)
|
||||||
|
.edges(excludingEdge: .top)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var headerImageView: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.image = UIImage(named: "Home/group_list_header")
|
||||||
|
view.contentMode = .scaleAspectFill
|
||||||
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var tableView: UITableView = {
|
lazy var tableView: UITableView = {
|
||||||
let tableView = UITableView(frame: .zero, style: .grouped)
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
tableView.backgroundColor = .white
|
tableView.backgroundColor = .clear
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
tableView.estimatedRowHeight = 68
|
tableView.rowHeight = Self.rowHeight
|
||||||
|
tableView.estimatedRowHeight = Self.rowHeight
|
||||||
tableView.showsVerticalScrollIndicator = false
|
tableView.showsVerticalScrollIndicator = false
|
||||||
tableView.bounces = false
|
tableView.bounces = false
|
||||||
tableView.register(GroupListCell.self)
|
tableView.register(GroupListCell.self)
|
||||||
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
|
|
||||||
tableView.dataSource = self
|
tableView.dataSource = self
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var createGroupBtn: UIButton = {
|
lazy var cancelBtn: UIButton = {
|
||||||
let btn = UIButton(type: .custom)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setTitle("创建圈子", for: .normal)
|
btn.setTitle("取消", for: .normal)
|
||||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
btn.setTitleColor(Self.primaryBlue, for: .normal)
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
btn.backgroundColor = .white
|
||||||
btn.cornerRadius = 22
|
btn.cornerRadius = 16
|
||||||
btn.addTarget(self, action: #selector(createAction), for: .touchUpInside)
|
btn.layer.borderWidth = 1
|
||||||
|
btn.layer.borderColor = Self.primaryBlue.cgColor
|
||||||
|
btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var joinGroupBtn: UIButton = {
|
lazy var confirmBtn: UIButton = {
|
||||||
let btn = UIButton(type: .custom)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setTitle("加入圈子", for: .normal)
|
btn.setTitle("确定", for: .normal)
|
||||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
btn.setTitleColor(.white, for: .normal)
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
btn.backgroundColor = Self.primaryBlue
|
||||||
btn.cornerRadius = 22
|
btn.cornerRadius = 16
|
||||||
btn.addTarget(self, action: #selector(joinAction), for: .touchUpInside)
|
btn.addTarget(self, action: #selector(confirmAction), for: .touchUpInside)
|
||||||
|
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -137,61 +123,41 @@ class GroupListPopView: UIView {
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
addSubview(bgView)
|
addSubview(bgView)
|
||||||
bgView.addSubview(infoView)
|
bgView.addSubview(infoView)
|
||||||
|
infoView.addSubview(headerImageView)
|
||||||
infoView.addSubview(tableView)
|
infoView.addSubview(tableView)
|
||||||
infoView.addSubview(createGroupBtn)
|
infoView.addSubview(cancelBtn)
|
||||||
infoView.addSubview(joinGroupBtn)
|
infoView.addSubview(confirmBtn)
|
||||||
|
|
||||||
bgView.addSubview(groupView)
|
bgView.layoutChain.edges()
|
||||||
groupView.addSubview(groupIconView)
|
|
||||||
groupView.addSubview(groupNameLab)
|
|
||||||
groupView.addSubview(groupArrowIconView)
|
|
||||||
|
|
||||||
groupView.layoutChain
|
|
||||||
.top(59)
|
|
||||||
.height(36)
|
|
||||||
.centerX()
|
|
||||||
.width(185, relation: .greaterThanOrEqual)
|
|
||||||
|
|
||||||
groupIconView.layoutChain
|
|
||||||
.left(11)
|
|
||||||
.centerY()
|
|
||||||
.width(30)
|
|
||||||
.height(30)
|
|
||||||
|
|
||||||
groupArrowIconView.layoutChain
|
|
||||||
.right(15)
|
|
||||||
.centerY()
|
|
||||||
.width(15)
|
|
||||||
.height(8.5)
|
|
||||||
|
|
||||||
groupNameLab.layoutChain
|
|
||||||
.edgesVertical()
|
|
||||||
.leftToRightOfView(groupIconView)
|
|
||||||
.rightToLeftOfView(groupArrowIconView)
|
|
||||||
|
|
||||||
infoView.layoutChain
|
infoView.layoutChain
|
||||||
.topToBottomOfView(groupView, offset: 20)
|
.bottom()
|
||||||
.edgesHorzontal(15)
|
.edgesHorzontal()
|
||||||
|
|
||||||
createGroupBtn.layoutChain
|
headerImageView.layoutChain
|
||||||
.left(15)
|
.top()
|
||||||
.bottom(20)
|
.edgesHorzontal()
|
||||||
.widthToView(joinGroupBtn)
|
.heightToWidth(127/375)
|
||||||
.height(44)
|
|
||||||
|
|
||||||
joinGroupBtn.layoutChain
|
cancelBtn.layoutChain
|
||||||
.leftToRightOfView(createGroupBtn, offset: 7)
|
.left(20)
|
||||||
.right(15)
|
.bottom(20 + kSafeBottomMargin)
|
||||||
.bottom(20)
|
.height(44)
|
||||||
|
.widthToView(confirmBtn)
|
||||||
|
|
||||||
|
confirmBtn.layoutChain
|
||||||
|
.leftToRightOfView(cancelBtn, offset: 12)
|
||||||
|
.right(20)
|
||||||
|
.bottomToView(cancelBtn)
|
||||||
.height(44)
|
.height(44)
|
||||||
.widthToView(joinGroupBtn)
|
|
||||||
|
|
||||||
tableView.layoutChain
|
tableView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.topToBottomOfView(headerImageView, offset: -20)
|
||||||
.height(78, relation: .greaterThanOrEqual)
|
.edgesHorzontal(12)
|
||||||
.bottomToTopOfView(createGroupBtn, offset: -17)
|
.height(Self.rowHeight)
|
||||||
|
.bottomToTopOfView(cancelBtn, offset: -16)
|
||||||
|
|
||||||
let tap = UITapGestureRecognizer(target: self, action: #selector(tap))
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask))
|
||||||
tap.delegate = self
|
tap.delegate = self
|
||||||
addGestureRecognizer(tap)
|
addGestureRecognizer(tap)
|
||||||
}
|
}
|
||||||
|
|
@ -200,61 +166,66 @@ class GroupListPopView: UIView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
private func updateTableHeight() {
|
||||||
super.layoutSubviews()
|
let visible = min(groupList.count, Self.maxVisibleRows)
|
||||||
|
let height = CGFloat(visible) * Self.rowHeight
|
||||||
|
tableView.layoutChain.height(height)
|
||||||
|
tableView.isScrollEnabled = groupList.count > Self.maxVisibleRows
|
||||||
|
tableView.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func slideOffTransform() -> CGAffineTransform {
|
||||||
|
layoutIfNeeded()
|
||||||
|
let offset = infoView.bounds.height// + 20 + kSafeBottomMargin
|
||||||
|
return CGAffineTransform(translationX: 0, y: max(offset, kScreenHeight * 0.5))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
extension GroupListPopView {
|
extension GroupListPopView {
|
||||||
|
|
||||||
/// 显示选择弹窗
|
/// 通过 window 从底部弹出圈子列表
|
||||||
/// - Parameters:
|
static func show(groupModel: GroupModel,
|
||||||
/// - start: 显示起始点
|
|
||||||
static func show(start: CGPoint,
|
|
||||||
groupModel: GroupModel,
|
|
||||||
completion: @escaping ((String?) -> Void)) {
|
completion: @escaping ((String?) -> Void)) {
|
||||||
guard let superView = kKeyWindow else {
|
guard let superView = kKeyWindow else { return }
|
||||||
return
|
|
||||||
|
let pop = GroupListPopView.shared
|
||||||
|
if pop.superview != nil {
|
||||||
|
pop.removeFromSuperview()
|
||||||
}
|
}
|
||||||
|
|
||||||
if GroupListPopView.shared.superview != nil {
|
pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
||||||
GroupListPopView.shared.removeFromSuperview()
|
pop.groupModel = groupModel
|
||||||
GroupListPopView.shared.bgView.frame = .zero
|
pop.completion = { text in
|
||||||
}
|
|
||||||
GroupListPopView.shared.groupModel = groupModel
|
|
||||||
GroupListPopView.shared.bgView.alpha = 1
|
|
||||||
GroupListPopView.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
|
||||||
superView.addSubview(GroupListPopView.shared)
|
|
||||||
superView.bringSubviewToFront(GroupListPopView.shared)
|
|
||||||
|
|
||||||
GroupListPopView.shared.infoView.alpha = 0
|
|
||||||
GroupListPopView.shared.completion = { (text) in
|
|
||||||
completion(text)
|
completion(text)
|
||||||
GroupListPopView.dismiss()
|
GroupListPopView.dismiss()
|
||||||
}
|
}
|
||||||
UIView.animate(withDuration: 0.25) {
|
|
||||||
GroupListPopView.shared.infoView.alpha = 1
|
pop.bgView.alpha = 0
|
||||||
// GroupListPopView.shared.infoView.dl.y = start.y
|
superView.addSubview(pop)
|
||||||
|
superView.bringSubviewToFront(pop)
|
||||||
|
pop.layoutIfNeeded()
|
||||||
|
pop.infoView.transform = pop.slideOffTransform()
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
||||||
|
pop.bgView.alpha = 1
|
||||||
|
pop.infoView.transform = .identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 关闭
|
/// 关闭(滑出底部)
|
||||||
static func dismiss() {
|
static func dismiss() {
|
||||||
guard GroupListPopView.shared.superview != nil else { return }
|
let pop = GroupListPopView.shared
|
||||||
let viewHeight = GroupListPopView.shared.infoView.frame.height
|
guard pop.superview != nil else { return }
|
||||||
UIView.animate(withDuration: 0.15) {
|
|
||||||
GroupListPopView.shared.infoView.alpha = 0
|
|
||||||
// GroupListPopView.shared.infoView.dl.y = -viewHeight
|
|
||||||
}
|
|
||||||
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
|
||||||
GroupListPopView.shared.bgView.alpha = 0
|
pop.bgView.alpha = 0
|
||||||
|
pop.infoView.transform = pop.slideOffTransform()
|
||||||
} completion: { _ in
|
} completion: { _ in
|
||||||
GroupListPopView.shared.removeFromSuperview()
|
pop.infoView.transform = .identity
|
||||||
|
pop.removeFromSuperview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UIGestureRecognizerDelegate
|
// MARK: - UIGestureRecognizerDelegate
|
||||||
extension GroupListPopView: UIGestureRecognizerDelegate {
|
extension GroupListPopView: UIGestureRecognizerDelegate {
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
|
@ -273,105 +244,178 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.configure(model: groupList[indexPath.row],
|
let model = groupList[indexPath.row]
|
||||||
isSelected: defaultGroupKey == groupList[indexPath.row].group_key)
|
cell.configure(model: model, isSelected: selectedGroupKey == model.group_key)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
completion?(groupList[indexPath.row].group_key)
|
selectedGroupKey = groupList[indexPath.row].group_key
|
||||||
|
tableView.reloadData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - GroupListCell
|
||||||
// MARK: - GroupListPopCell
|
|
||||||
class GroupListCell: UITableViewCell {
|
class GroupListCell: UITableViewCell {
|
||||||
|
|
||||||
func configure(model: GroupInfoModel, isSelected: Bool) {
|
func configure(model: GroupInfoModel, isSelected: Bool) {
|
||||||
avaterImgView.image = model.groupIcon
|
avaterImgView.image = model.groupIcon
|
||||||
nameLab.text = model.name
|
nameLab.text = model.name
|
||||||
bgView.isHidden = !isSelected
|
countLab.text = "共\(model.people_no)成员"
|
||||||
|
|
||||||
|
tagStack.arrangedSubviews.forEach {
|
||||||
|
tagStack.removeArrangedSubview($0)
|
||||||
|
$0.removeFromSuperview()
|
||||||
|
}
|
||||||
|
let labels = Array(model.labels.prefix(2))
|
||||||
|
tagStack.isHidden = labels.isEmpty
|
||||||
|
for text in labels {
|
||||||
|
tagStack.addArrangedSubview(Self.makeTagLabel(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
cardView.backgroundColor = isSelected ? GroupListPopView.cardBgColor : .white
|
||||||
|
cardView.layer.borderWidth = isSelected ? 1 : 0
|
||||||
|
cardView.layer.borderColor = GroupListPopView.primaryBlueColor.cgColor
|
||||||
|
|
||||||
|
checkIcon.image = isSelected
|
||||||
|
? UIImage(systemName: "checkmark.circle.fill")
|
||||||
|
: UIImage(systemName: "circle")
|
||||||
|
checkIcon.tintColor = isSelected
|
||||||
|
? GroupListPopView.primaryBlueColor
|
||||||
|
: UIColor(hexStr: "#D0D0D0")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeTagLabel(_ text: String) -> UILabel {
|
||||||
|
let label = PaddingLabel()
|
||||||
|
label.text = text
|
||||||
|
label.textColor = GroupListPopView.tagTextColor
|
||||||
|
label.font = .systemFont(ofSize: 10, weight: .medium)
|
||||||
|
label.backgroundColor = GroupListPopView.tagBgColor
|
||||||
|
label.cornerRadius = 4
|
||||||
|
label.clipsToBounds = true
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(style: CellStyle, reuseIdentifier: String?) {
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
contentView.backgroundColor = .clear
|
||||||
setupSubviews()
|
setupSubviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupSubviews() {
|
private func setupSubviews() {
|
||||||
contentView.addSubview(bgView)
|
contentView.addSubview(cardView)
|
||||||
contentView.addSubview(avaterImgView)
|
cardView.addSubview(avaterImgView)
|
||||||
contentView.addSubview(nameLab)
|
cardView.addSubview(nameLab)
|
||||||
|
cardView.addSubview(tagStack)
|
||||||
|
cardView.addSubview(countLab)
|
||||||
|
cardView.addSubview(checkIcon)
|
||||||
|
|
||||||
bgView.layoutChain.edges()
|
cardView.layoutChain
|
||||||
|
.edgesVertical(4)
|
||||||
|
.edgesHorzontal(4)
|
||||||
|
|
||||||
avaterImgView.layoutChain
|
avaterImgView.layoutChain
|
||||||
.edgesVertical(14)
|
.left(12)
|
||||||
.left(15)
|
.centerY()
|
||||||
.width(40)
|
.width(44)
|
||||||
.height(40)
|
.height(44)
|
||||||
|
|
||||||
|
checkIcon.layoutChain
|
||||||
|
.right(12)
|
||||||
|
.centerY()
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
|
||||||
nameLab.layoutChain
|
nameLab.layoutChain
|
||||||
.centerY(avaterImgView)
|
.topToView(avaterImgView, offset: 2)
|
||||||
.leftToRightOfView(avaterImgView, offset: 11)
|
.leftToRightOfView(avaterImgView, offset: 10)
|
||||||
.right(100, relation: .greaterThanOrEqual)
|
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
|
||||||
|
|
||||||
|
tagStack.layoutChain
|
||||||
|
.leftToRightOfView(nameLab, offset: 6)
|
||||||
|
.centerY(nameLab)
|
||||||
|
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
|
||||||
|
|
||||||
|
countLab.layoutChain
|
||||||
|
.topToBottomOfView(nameLab, offset: 4)
|
||||||
|
.leftToView(nameLab)
|
||||||
|
.rightToLeftOfView(checkIcon, offset: -8)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
lazy var cardView: UIView = {
|
||||||
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 bgView: UIView = {
|
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
|
view.backgroundColor = .white
|
||||||
|
view.cornerRadius = 12
|
||||||
let lineView = UIView()
|
view.clipsToBounds = true
|
||||||
lineView.backgroundColor = UIColor(hexStr: "#16B3FF")
|
|
||||||
view.addSubview(lineView)
|
|
||||||
lineView.layoutChain
|
|
||||||
.left()
|
|
||||||
.edgesVertical()
|
|
||||||
.width(6)
|
|
||||||
|
|
||||||
let selectedIcon = UIImageView()
|
|
||||||
selectedIcon.image = UIImage(named: "Group/selected")
|
|
||||||
view.addSubview(selectedIcon)
|
|
||||||
selectedIcon.layoutChain
|
|
||||||
.right(14)
|
|
||||||
.centerY()
|
|
||||||
.width(24)
|
|
||||||
.height(24)
|
|
||||||
|
|
||||||
view.isHidden = true
|
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var avaterImgView: UIImageView = {
|
lazy var avaterImgView: UIImageView = {
|
||||||
let view = UIImageView()
|
let view = UIImageView()
|
||||||
// view.backgroundColor = .lightGray
|
|
||||||
view.contentMode = .scaleAspectFill
|
view.contentMode = .scaleAspectFill
|
||||||
// view.cornerRadius = 20
|
view.cornerRadius = 8
|
||||||
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var nameLab: UILabel = {
|
lazy var nameLab: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
label.textColor = UIColor(hexStr: "#353B4F")
|
||||||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
lazy var tagStack: UIStackView = {
|
||||||
|
let view = UIStackView()
|
||||||
|
view.axis = .horizontal
|
||||||
|
view.alignment = .center
|
||||||
|
view.spacing = 4
|
||||||
|
view.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var countLab: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = UIColor(hexStr: "#999999")
|
||||||
|
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var checkIcon: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.contentMode = .scaleAspectFit
|
||||||
|
view.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Color accessors for cell (file-private via static)
|
||||||
|
private extension GroupListPopView {
|
||||||
|
static var primaryBlueColor: UIColor { primaryBlue }
|
||||||
|
static var cardBgColor: UIColor { cardBg }
|
||||||
|
static var tagBgColor: UIColor { tagBg }
|
||||||
|
static var tagTextColor: UIColor { tagText }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 带内边距的标签
|
||||||
|
private class PaddingLabel: UILabel {
|
||||||
|
private let inset = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
|
||||||
|
|
||||||
|
override func drawText(in rect: CGRect) {
|
||||||
|
super.drawText(in: rect.inset(by: inset))
|
||||||
|
}
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
let size = super.intrinsicContentSize
|
||||||
|
return CGSize(width: size.width + inset.left + inset.right,
|
||||||
|
height: size.height + inset.top + inset.bottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||