|
|
@ -96,6 +96,7 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.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 */; };
|
||||
305A771D2FCA8C7000227D26 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76862FCA8C7000227D26 /* ViewController.swift */; };
|
||||
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 */; };
|
||||
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 305A767F2FCA8C7000227D26 /* Assets.xcassets */; };
|
||||
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 */; };
|
||||
55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.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 */; };
|
||||
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -397,6 +399,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -605,6 +608,9 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -784,6 +790,7 @@
|
|||
305A75062FCA8C7000227D26 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55B217A23022F0A000784703 /* Font */,
|
||||
305A75042FCA8C7000227D26 /* Extension */,
|
||||
305A75052FCA8C7000227D26 /* Wrapper.swift */,
|
||||
);
|
||||
|
|
@ -950,7 +957,8 @@
|
|||
305A75392FCA8C7000227D26 /* Font */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */,
|
||||
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */,
|
||||
55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */,
|
||||
305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */,
|
||||
);
|
||||
path = Font;
|
||||
|
|
@ -989,6 +997,7 @@
|
|||
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
|
||||
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
||||
305A76232FCA8C7000227D26 /* GroupView.swift */,
|
||||
55B217B03022F0A000784710 /* GroupItineraryView.swift */,
|
||||
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
|
||||
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
|
||||
307073E42FD18A20004C37CC /* GroupChat */,
|
||||
|
|
@ -1666,6 +1675,14 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
55B217A23022F0A000784703 /* Font */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55B217A03022F0A000784701 /* FontManager.swift */,
|
||||
);
|
||||
path = Font;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B07D45692FCECE07570D9B73 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1744,11 +1761,12 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
305A771E2FCA8C7000227D26 /* adhoc.xcconfig in Resources */,
|
||||
305A771F2FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf in Resources */,
|
||||
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */,
|
||||
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */,
|
||||
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */,
|
||||
55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */,
|
||||
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */,
|
||||
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -1911,6 +1929,7 @@
|
|||
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */,
|
||||
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */,
|
||||
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */,
|
||||
55B217A13022F0A000784702 /* FontManager.swift in Sources */,
|
||||
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */,
|
||||
305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */,
|
||||
305A76B32FCA8C7000227D26 /* UILabel+Extension.swift in Sources */,
|
||||
|
|
@ -1999,6 +2018,7 @@
|
|||
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */,
|
||||
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */,
|
||||
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */,
|
||||
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */,
|
||||
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
|
||||
30BAB8512FCD331C00C33B5C /* GroupAPI.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 {
|
||||
|
||||
UIFont.loadFontName("douyu.otf")
|
||||
UIFont.loadFontName("zihunbiantaoti-r.ttf")
|
||||
FontManager.registerCustomFonts()
|
||||
setupLocation()
|
||||
ApiManager.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? {
|
||||
// 字体文件资源路径
|
||||
guard let fontURL = Bundle.main.url(forResource: fileName, withExtension: nil),
|
||||
let nsName = fileName as NSString
|
||||
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 provider = CGDataProvider(data: data as CFData),
|
||||
let font = CGFont(provider) else { return nil }
|
||||
|
||||
// 注册字体
|
||||
CTFontManagerRegisterGraphicsFont(font, nil)
|
||||
return String(font.fullName ?? "")
|
||||
var error: Unmanaged<CFError>?
|
||||
CTFontManagerRegisterGraphicsFont(font, &error)
|
||||
|
||||
// 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
|
||||
//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>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>douyu.otf</string>
|
||||
<string>荆南波波黑-Bold.ttf</string>
|
||||
<string>优设标题黑_猫啃网.ttf</string>
|
||||
<string>zihunbiantaoti-r.ttf</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ final class MainTabBarController: UITabBarController {
|
|||
private lazy var mineNav = BaseNavigationController(rootViewController: MineViewController())
|
||||
|
||||
private lazy var navControllers: [BaseNavigationController] = {
|
||||
[locationNav, exploreNav, circlesNav, mineNav]
|
||||
[locationNav, circlesNav, exploreNav, mineNav]
|
||||
}()
|
||||
|
||||
// MARK: - Custom Tab Bar
|
||||
|
|
@ -25,16 +25,16 @@ final class MainTabBarController: UITabBarController {
|
|||
image: UIImage(named: "Tabbar/location"),
|
||||
selectedImage: UIImage(named: "Tabbar/location_selected")
|
||||
),
|
||||
QuickLocationTabBar.TabItem(
|
||||
title: "探索",
|
||||
image: UIImage(named: "Tabbar/explore"),
|
||||
selectedImage: UIImage(named: "Tabbar/explore_selected")
|
||||
),
|
||||
QuickLocationTabBar.TabItem(
|
||||
title: "圈子",
|
||||
image: UIImage(named: "Tabbar/group"),
|
||||
selectedImage: UIImage(named: "Tabbar/group_selected")
|
||||
),
|
||||
QuickLocationTabBar.TabItem(
|
||||
title: "探索",
|
||||
image: UIImage(named: "Tabbar/explore"),
|
||||
selectedImage: UIImage(named: "Tabbar/explore_selected")
|
||||
),
|
||||
QuickLocationTabBar.TabItem(
|
||||
title: "我的",
|
||||
image: UIImage(named: "Tabbar/mine"),
|
||||
|
|
|
|||
|
|
@ -27,15 +27,14 @@ final class QuickLocationTabBar: UIView {
|
|||
private let barContentHeight: CGFloat = 49
|
||||
private let containerCornerRadius: CGFloat = 0
|
||||
private let horizontalInset: CGFloat = 0
|
||||
private let iconSize: CGFloat = 32
|
||||
private let iconSize: CGFloat = 24
|
||||
private let iconLabelSpacing: CGFloat = 4
|
||||
private let labelHeight: CGFloat = 11
|
||||
|
||||
private let selectedTextColor = UIColor(hexStr: "#1A1A1A")
|
||||
private let unselectedTextColor = UIColor(hexStr: "#AAAAAA")
|
||||
private let selectedTextColor = UIColor(hexStr: "#2A3648")
|
||||
private let unselectedTextColor = UIColor(hexStr: "#9BA0AF")
|
||||
|
||||
private let selectedFont = UIFont(name: "PingFangSC-Semibold", size: 11)
|
||||
?? UIFont.systemFont(ofSize: 11, weight: .semibold)
|
||||
private let selectedFont = UIFont.systemFont(ofSize: 11, weight: .bold)
|
||||
private let unselectedFont = UIFont.systemFont(ofSize: 11, weight: .regular)
|
||||
|
||||
// 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 成员
|
||||
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
|
||||
vipIcon.image = model.vipIcon
|
||||
// vipIcon.image = model.vipIcon
|
||||
vipBgImgView.isHidden = model.level == 1
|
||||
nameLab.text = model.nick_name
|
||||
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
|
||||
selectedBgView.isHidden = !isSelected
|
||||
updateSOSBlink(isSOSBlinking)
|
||||
// 会员权益
|
||||
// if AppContextManager.shared.vip > 1, model.is_online {
|
||||
// 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() {
|
||||
contentView.addSubview(selectedBgView)
|
||||
contentView.addSubview(avaterImgView)
|
||||
contentView.addSubview(vipIcon)
|
||||
contentView.addSubview(sosImgView)
|
||||
contentView.addSubview(vipBgImgView)
|
||||
// contentView.addSubview(vipIcon)
|
||||
contentView.addSubview(batteryInfoView)
|
||||
batteryInfoView.addSubview(cornerView)
|
||||
cornerView.addSubview(batteryView)
|
||||
|
|
@ -688,6 +718,16 @@ class GroupMemberListCell: UICollectionViewCell {
|
|||
.edgesHorzontal(5.5)
|
||||
.heightToWidth(1)
|
||||
|
||||
sosImgView.layoutChain
|
||||
.topToView(avaterImgView)
|
||||
.leftToView(avaterImgView)
|
||||
.rightToView(avaterImgView)
|
||||
.bottomToView(avaterImgView)
|
||||
|
||||
vipBgImgView.layoutChain
|
||||
.top(7)
|
||||
.centerX()
|
||||
|
||||
batteryInfoView.layoutChain
|
||||
.leftToView(avaterImgView)
|
||||
.rightToView(avaterImgView)
|
||||
|
|
@ -712,14 +752,14 @@ class GroupMemberListCell: UICollectionViewCell {
|
|||
.right(5)
|
||||
.centerY()
|
||||
|
||||
vipIcon.layoutChain
|
||||
.topToView(avaterImgView, offset: -8)
|
||||
.leftToView(avaterImgView, offset: -6)
|
||||
.width(25)
|
||||
.height(21)
|
||||
// vipIcon.layoutChain
|
||||
// .topToView(avaterImgView, offset: -8)
|
||||
// .leftToView(avaterImgView, offset: -6)
|
||||
// .width(25)
|
||||
// .height(21)
|
||||
|
||||
nameLab.layoutChain
|
||||
.topToBottomOfView(batteryInfoView, offset: 4)
|
||||
.topToBottomOfView(batteryInfoView, offset: 8)
|
||||
.edgesHorzontal()
|
||||
|
||||
|
||||
|
|
@ -732,6 +772,13 @@ class GroupMemberListCell: UICollectionViewCell {
|
|||
return view
|
||||
}()
|
||||
|
||||
lazy var vipBgImgView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "GroupMemberList/vip")
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var avaterImgView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.backgroundColor = .lightGray
|
||||
|
|
@ -740,6 +787,13 @@ class GroupMemberListCell: UICollectionViewCell {
|
|||
return view
|
||||
}()
|
||||
|
||||
lazy var sosImgView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "GroupMemberList/sos")
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var batteryInfoView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
|
|
|
|||
|
|
@ -24,133 +24,228 @@ class GroupView: UIView {
|
|||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
/// 0 = 圈子,1 = 行程
|
||||
private(set) var currentTopTab: Int = 0
|
||||
|
||||
/// 当前是否吸顶
|
||||
private var isSticky = false
|
||||
/// 内部 tableview 是否可滚动
|
||||
private var isSubCanScroll = false
|
||||
|
||||
var stickThreshold: CGFloat {
|
||||
return segmentView.dl.y
|
||||
// segment 在 listContainer 内,吸顶以 listContainer 顶部为准
|
||||
return listContainer.dl.y
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
private func setupUI() {
|
||||
addSubview(navBgView)
|
||||
addSubview(navView)
|
||||
navView.addRightButton(scanBtn)
|
||||
addSubview(topBar)
|
||||
topBar.addSubview(circleTabLabel)
|
||||
topBar.addSubview(itineraryTabLabel)
|
||||
topBar.addSubview(messageBtn)
|
||||
|
||||
addSubview(mainScrollView)
|
||||
addSubview(circlePage)
|
||||
circlePage.addSubview(mainScrollView)
|
||||
mainScrollView.addSubview(contentView)
|
||||
contentView.addSubview(cycleScrollView)
|
||||
contentView.addSubview(actionButtonsView)
|
||||
actionButtonsView.addSubview(createGroupBtn)
|
||||
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(hotGroupsCollectionView)
|
||||
contentView.addSubview(segmentView)
|
||||
segmentView.addSubview(createdTabLabel)
|
||||
segmentView.addSubview(joinedTabLabel)
|
||||
segmentView.addSubview(tabIndicator)
|
||||
contentView.addSubview(segmentScrollView)
|
||||
segmentScrollView.addSubview(segmentContentView)
|
||||
segmentContentView.addSubview(createdTableView)
|
||||
segmentContentView.addSubview(joinedTableView)
|
||||
cycleScrollView.isHidden = true
|
||||
hotGroupTitleLabel.isHidden = true
|
||||
hotGroupsCollectionView.isHidden = true
|
||||
|
||||
addSubview(itineraryPage)
|
||||
itineraryPage.isHidden = true
|
||||
|
||||
navBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(kNaviHeight)
|
||||
.heightToWidth(160/375)
|
||||
|
||||
navView.layoutChain
|
||||
topBar.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(kNaviHeight)
|
||||
|
||||
mainScrollView.layoutChain
|
||||
.topToBottomOfView(navBgView)
|
||||
circleTabLabel.layoutChain
|
||||
.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)
|
||||
|
||||
itineraryPage.layoutChain
|
||||
.topToBottomOfView(topBar)
|
||||
.edges(excludingEdge: .top)
|
||||
|
||||
mainScrollView.layoutChain
|
||||
.edges()
|
||||
|
||||
contentView.layoutChain
|
||||
.edges()
|
||||
.widthToView(mainScrollView)
|
||||
|
||||
// 轮播图
|
||||
cycleScrollView.layoutChain
|
||||
.top(10)
|
||||
.left(15).right(15)
|
||||
.heightToWidth(100/375)
|
||||
|
||||
// 创建/加入圈子
|
||||
actionButtonsView.layoutChain
|
||||
.topToBottomOfView(cycleScrollView, offset: 38)
|
||||
.edgesHorzontal()
|
||||
.top(12)
|
||||
.edgesHorzontal(16)
|
||||
.height(108)
|
||||
|
||||
createGroupBtn.layoutChain
|
||||
.left(15)
|
||||
.left()
|
||||
.edgesVertical()
|
||||
.widthToView(joinGroupBtn)
|
||||
.height(40)
|
||||
|
||||
joinGroupBtn.layoutChain
|
||||
.topToView(createGroupBtn)
|
||||
.bottomToView(createGroupBtn)
|
||||
.leftToRightOfView(createGroupBtn, offset: 21)
|
||||
.right(15)
|
||||
.widthToView(joinGroupBtn)
|
||||
.leftToRightOfView(createGroupBtn, offset: 12)
|
||||
.right()
|
||||
.widthToView(createGroupBtn)
|
||||
|
||||
// 热门酷圈
|
||||
hotGroupTitleLabel.layoutChain
|
||||
.topToBottomOfView(actionButtonsView, offset: 21)
|
||||
.left(16)
|
||||
|
||||
hotGroupsCollectionView.layoutChain
|
||||
.topToBottomOfView(hotGroupTitleLabel, offset: 8)
|
||||
listContainer.layoutChain
|
||||
.topToBottomOfView(actionButtonsView, offset: 16)
|
||||
.edgesHorzontal()
|
||||
.height(88)
|
||||
.bottom()
|
||||
|
||||
// Segment
|
||||
let labelWidth = (UIScreen.main.bounds.width - 60) / 2
|
||||
segmentView.layoutChain
|
||||
.topToBottomOfView(hotGroupsCollectionView)
|
||||
.top()
|
||||
.edgesHorzontal()
|
||||
.height(44)
|
||||
|
||||
createdTabLabel.layoutChain
|
||||
joinedTabLabel.layoutChain
|
||||
.left(30).centerY()
|
||||
.width(labelWidth)
|
||||
|
||||
joinedTabLabel.layoutChain
|
||||
createdTabLabel.layoutChain
|
||||
.right(30).centerY()
|
||||
.width(labelWidth)
|
||||
|
||||
tabIndicator.layoutChain
|
||||
.bottom().centerX(createdTabLabel)
|
||||
.width(14).height(3)
|
||||
.topToBottomOfView(joinedTabLabel, offset: 4)
|
||||
.centerX(joinedTabLabel)
|
||||
.width(19).height(4)
|
||||
|
||||
// 横向 segmentScrollView
|
||||
segmentScrollView.layoutChain
|
||||
.topToBottomOfView(segmentView)
|
||||
.edgesHorzontal()
|
||||
.bottom()
|
||||
.height(kScreenHeight - kNaviHeight - 44)
|
||||
.height(kScreenHeight - kNaviHeight - 44 - 12 - 108 - 16)
|
||||
|
||||
segmentContentView.layoutChain
|
||||
.edges()
|
||||
.heightToView(segmentScrollView)
|
||||
|
||||
let svWidth = UIScreen.main.bounds.width
|
||||
createdTableView.layoutChain
|
||||
joinedTableView.layoutChain
|
||||
.top().bottom().left()
|
||||
.width(svWidth)
|
||||
|
||||
joinedTableView.layoutChain
|
||||
createdTableView.layoutChain
|
||||
.top().bottom()
|
||||
.leftToRightOfView(createdTableView)
|
||||
.leftToRightOfView(joinedTableView)
|
||||
.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() {
|
||||
super.layoutSubviews()
|
||||
segmentContentView.layoutChain.width(bounds.width * 2)
|
||||
|
||||
listContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
listContainer.layer.cornerRadius = 20
|
||||
listContainer.clipsToBounds = true
|
||||
}
|
||||
|
||||
// MARK: - Nav
|
||||
|
|
@ -161,9 +256,41 @@ class GroupView: UIView {
|
|||
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 = {
|
||||
let nav = BaseNavigationView(title: "圈子")
|
||||
nav.backBtn.isHidden = true
|
||||
nav.isHidden = true
|
||||
return nav
|
||||
}()
|
||||
|
||||
|
|
@ -174,6 +301,18 @@ class GroupView: UIView {
|
|||
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
|
||||
lazy var mainScrollView: PanScrollView = {
|
||||
let sv = PanScrollView()
|
||||
|
|
@ -190,7 +329,7 @@ class GroupView: UIView {
|
|||
return v
|
||||
}()
|
||||
|
||||
// MARK: - 轮播图
|
||||
// MARK: - 轮播图(隐藏保留绑定)
|
||||
lazy var cycleScrollView: SDCycleScrollView = {
|
||||
let view = SDCycleScrollView(frame: .zero)
|
||||
view.backgroundColor = .lightGray
|
||||
|
|
@ -206,34 +345,26 @@ class GroupView: UIView {
|
|||
return view
|
||||
}()
|
||||
|
||||
// MARK: - 创建 / 加入 圈子
|
||||
// MARK: - 创建 / 加入 圈子(色卡,整卡可点)
|
||||
private lazy var actionButtonsView: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .clear
|
||||
return v
|
||||
}()
|
||||
|
||||
lazy var createGroupBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("创建圈子", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
||||
btn.cornerRadius = 20
|
||||
return btn
|
||||
lazy var createGroupBtn: UIView = {
|
||||
let v = UIView()
|
||||
v.isUserInteractionEnabled = true
|
||||
return v
|
||||
}()
|
||||
|
||||
lazy var joinGroupBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("加入圈子", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||||
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: UIView = {
|
||||
let v = UIView()
|
||||
v.isUserInteractionEnabled = true
|
||||
return v
|
||||
}()
|
||||
|
||||
// MARK: - 热门酷圈
|
||||
// MARK: - 热门酷圈(隐藏保留绑定)
|
||||
private lazy var hotGroupTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "热门酷圈"
|
||||
|
|
@ -255,37 +386,46 @@ class GroupView: UIView {
|
|||
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 = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = .white
|
||||
|
||||
let line = UIView()
|
||||
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
|
||||
v.addSubview(line)
|
||||
line.layoutChain
|
||||
.edgesHorzontal()
|
||||
.height(1)
|
||||
.bottom(4)
|
||||
// v.addSubview(line)
|
||||
// line.layoutChain
|
||||
// .edgesHorzontal()
|
||||
// .height(1)
|
||||
// .bottom(4)
|
||||
|
||||
return v
|
||||
}()
|
||||
|
||||
private(set) lazy var createdTabLabel: UILabel = {
|
||||
/// 「全部圈子」→ joined 列表(index 0)
|
||||
private(set) lazy var joinedTabLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "我创建的圈子"
|
||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
label.textColor = UIColor(hexStr: "#1A1A1A")
|
||||
label.text = "全部圈子"
|
||||
label.font = .systemFont(ofSize: 16, weight: .heavy)
|
||||
label.textColor = UIColor(hexStr: "#2A3648")
|
||||
label.textAlignment = .center
|
||||
label.isUserInteractionEnabled = true
|
||||
return label
|
||||
}()
|
||||
|
||||
private(set) lazy var joinedTabLabel: UILabel = {
|
||||
/// 「我的圈子」→ created 列表(index 1)
|
||||
private(set) lazy var createdTabLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "我加入的圈子"
|
||||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.text = "我的圈子"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#2A3648")
|
||||
label.textAlignment = .center
|
||||
label.isUserInteractionEnabled = true
|
||||
return label
|
||||
|
|
@ -293,7 +433,7 @@ class GroupView: UIView {
|
|||
|
||||
private lazy var tabIndicator: UIView = {
|
||||
let v = UIView()
|
||||
v.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||
v.backgroundColor = UIColor(hexStr: "#85D8FF")
|
||||
v.layer.cornerRadius = 2
|
||||
return v
|
||||
}()
|
||||
|
|
@ -342,25 +482,40 @@ class GroupView: UIView {
|
|||
// MARK: - Segment
|
||||
var currentSegmentIndex: Int = 0
|
||||
|
||||
/// index: 0=全部圈子(joined),1=我的圈子(created)
|
||||
func selectSegment(at index: Int) {
|
||||
currentSegmentIndex = index
|
||||
let isCreated = index == 0
|
||||
let isAll = index == 0
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.createdTabLabel.textColor = isCreated ? UIColor(hexStr: "#1A1A1A") : UIColor(hexStr: "#999999")
|
||||
self.joinedTabLabel.textColor = isCreated ? UIColor(hexStr: "#999999") : UIColor(hexStr: "#1A1A1A")
|
||||
self.joinedTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648") : UIColor(hexStr: "#2A3648", alpha: 0.5)
|
||||
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.createdTabLabel.center.x
|
||||
: self.joinedTabLabel.center.x
|
||||
self.tabIndicator.center.x = isAll
|
||||
? self.joinedTabLabel.center.x
|
||||
: self.createdTabLabel.center.x
|
||||
|
||||
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) {
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .white
|
||||
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||
setupUI()
|
||||
}
|
||||
|
||||
|
|
@ -586,11 +741,6 @@ final class CircleGroupCell: UITableViewCell {
|
|||
|
||||
func configure(_ model: GroupCellData) {
|
||||
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
|
||||
nameLabel.text = g.groupName ?? ""
|
||||
countLabel.text = "共\(g.memberCount)个成员"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import RxCocoa
|
|||
import RxDataSources
|
||||
import SDCycleScrollView
|
||||
import OpenIMSDK
|
||||
import ObjectMapper
|
||||
|
||||
final class GroupViewController: BaseViewController {
|
||||
|
||||
|
|
@ -17,6 +18,10 @@ final class GroupViewController: BaseViewController {
|
|||
fileprivate var rootView: GroupView!
|
||||
private let viewModel = GroupViewModel()
|
||||
|
||||
/// 行程页当前圈子数据
|
||||
private var itineraryGroupModel: GroupModel?
|
||||
private var itinerarySchedules: [ScheduleModel] = []
|
||||
|
||||
override func loadView() {
|
||||
rootView = GroupView(frame: UIScreen.main.bounds)
|
||||
view = rootView
|
||||
|
|
@ -27,6 +32,7 @@ final class GroupViewController: BaseViewController {
|
|||
bindViewModel()
|
||||
reactiveAction()
|
||||
observeTableViews()
|
||||
bindItineraryPage()
|
||||
requestRecommandGroup()
|
||||
requestGroupInfo()
|
||||
|
||||
|
|
@ -123,14 +129,30 @@ final class GroupViewController: BaseViewController {
|
|||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.createdTabLabel.rx.tapGesture
|
||||
// 子 Tab:全部圈子(0=joined) / 我的圈子(1=created)
|
||||
rootView.joinedTabLabel.rx.tapGesture
|
||||
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.joinedTabLabel.rx.tapGesture
|
||||
rootView.createdTabLabel.rx.tapGesture
|
||||
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(1) })
|
||||
.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(
|
||||
rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(),
|
||||
rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable()
|
||||
|
|
@ -159,12 +181,90 @@ final class GroupViewController: BaseViewController {
|
|||
.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) {
|
||||
rootView.selectSegment(at: index)
|
||||
let offset = CGPoint(x: CGFloat(index) * rootView.segmentScrollView.bounds.width, y: 0)
|
||||
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 滚动
|
||||
private func observeTableViews() {
|
||||
rootView.createdTableView.rx.didScroll
|
||||
|
|
|
|||
|
|
@ -86,8 +86,9 @@ final class GroupViewModel {
|
|||
return m
|
||||
}()
|
||||
|
||||
// 「全部圈子」= 全部已加入群;「我的圈子」= 我创建的
|
||||
var all: [GroupCellData] = []
|
||||
var created: [GroupCellData] = []
|
||||
var joined: [GroupCellData] = []
|
||||
|
||||
for g in cachedGroups {
|
||||
let conv = convMap[g.groupID ?? ""]
|
||||
|
|
@ -97,15 +98,14 @@ final class GroupViewModel {
|
|||
lastMsgTime: Int(conv?.latestMsgSendTime ?? 0),
|
||||
localIcon: getLocalIcon(groupId: g.groupID ?? "")
|
||||
)
|
||||
all.append(data)
|
||||
if g.ownerUserID == currentUserId || g.creatorUserID == currentUserId {
|
||||
created.append(data)
|
||||
} else {
|
||||
joined.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
createdSectionsSubject.onNext(created.mapSection())
|
||||
joinedSectionsSubject.onNext(joined.mapSection())
|
||||
joinedSectionsSubject.onNext(all.mapSection())
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ObjectMapper
|
|||
import RxSwift
|
||||
import RxCocoa
|
||||
import Kingfisher
|
||||
import Photos
|
||||
|
||||
class InviteJoinVC: BaseViewController {
|
||||
|
||||
|
|
@ -25,16 +26,24 @@ class InviteJoinVC: BaseViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
|
||||
// 分享
|
||||
rootView.shareAppBtn.rx.tap.subscribe(onNext: { _ in
|
||||
SharePopView.show()
|
||||
// 分享邀请卡片到微信
|
||||
rootView.shareBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
self?.shareInviteCard()
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
// 分享邀请码
|
||||
rootView.shareCodeBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// 下载图片到相册
|
||||
rootView.downloadBtn.rx.tap.subscribe(onNext: { [weak self] in
|
||||
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,
|
||||
let shareConfig = config.shareConfig else { return }
|
||||
|
||||
|
|
@ -50,7 +59,7 @@ class InviteJoinVC: BaseViewController {
|
|||
guard let self = self else { return }
|
||||
DLToast.dismiss()
|
||||
|
||||
guard let screenshot = self.rootView.inviteInfoView.takeScreenshot(),
|
||||
guard let screenshot = self.rootView.inviteCardView.takeScreenshot(),
|
||||
let imageData = screenshot.pngData() else {
|
||||
DLToast.showError(text: "分享失败")
|
||||
return
|
||||
|
|
@ -87,24 +96,68 @@ class InviteJoinVC: BaseViewController {
|
|||
} else {
|
||||
sendMessage(fallbackImage)
|
||||
}
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
guard let model = groupInModel else { return }
|
||||
rootView.groupIcon.image = model.groupIcon
|
||||
rootView.groupNameLab.text = model.name
|
||||
requestOperateGroup(key: model.group_key)
|
||||
private func downloadInviteCard() {
|
||||
guard let screenshot = rootView.inviteCardView.takeScreenshot() else {
|
||||
DLToast.showError(text: "保存失败")
|
||||
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
|
||||
private func requestOperateGroup(key: String) {
|
||||
DLToast.showLoading()
|
||||
GroupService.operate(opType: "share", requestData: ["group_key" : key]).subscribe(onNext: { [weak self] response in
|
||||
GroupService.operate(opType: "share", requestData: ["group_key": key]).subscribe(onNext: { [weak self] response in
|
||||
guard let self = self,
|
||||
let data = response.data,
|
||||
let code = data["share_code"] as? String else { return }
|
||||
self.rootView.inviteCodeLab.text = code
|
||||
self.rootView.qrCodeImgView.image = code.createQRCode(size: self.rootView.qrCodeImgView.dl.size)
|
||||
}, onError: { (error) in
|
||||
self.rootView.qrCodeImgView.image = code.createQRCode(size: CGSize(width: 120, height: 120))
|
||||
}, onError: { _ in
|
||||
DLToast.show(text: "获取错误,请重试") {
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}
|
||||
|
|
@ -120,5 +173,4 @@ class InviteJoinVC: BaseViewController {
|
|||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,154 +13,220 @@ class InviteJoinView: UIView {
|
|||
|
||||
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
|
||||
guard let code = self.inviteCodeLab.text else { return }
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
DLToast.showSuccess(text: "已复制")
|
||||
}.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
addSubview(navBgView)
|
||||
addSubview(navView)
|
||||
addSubview(groupIcon)
|
||||
addSubview(groupNameLab)
|
||||
addSubview(inviteInfoView)
|
||||
inviteInfoView.addSubview(inviteInfoTitleLab)
|
||||
inviteInfoView.addSubview(inviteTipsLab)
|
||||
inviteInfoView.addSubview(inviteCodeView)
|
||||
addSubview(inviteCardView)
|
||||
inviteCardView.addSubview(cardBgImg)
|
||||
inviteCardView.addSubview(groupView)
|
||||
inviteCardView.addSubview(inviteInfoTitleLab)
|
||||
inviteCardView.addSubview(inviteCodeView)
|
||||
inviteCodeView.addSubview(inviteCodeTipsLab)
|
||||
inviteCodeView.addSubview(inviteCodeLab)
|
||||
inviteInfoView.addSubview(scanTipsLab)
|
||||
inviteInfoView.addSubview(qrCodeView)
|
||||
qrCodeView.addSubview(qrCodeBgImg)
|
||||
inviteCardView.addSubview(inviteTipsLab)
|
||||
inviteCardView.addSubview(scanTipsLab)
|
||||
inviteCardView.addSubview(qrCodeView)
|
||||
qrCodeView.addSubview(qrCodeImgView)
|
||||
addSubview(shareAppBtn)
|
||||
addSubview(shareCodeBtn)
|
||||
|
||||
navBgView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.heightToWidth(160/375)
|
||||
addSubview(shareBtn)
|
||||
addSubview(downloadBtn)
|
||||
|
||||
navView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(kNaviHeight)
|
||||
|
||||
groupIcon.layoutChain
|
||||
.topToBottomOfView(navView, offset: 24)
|
||||
.width(80)
|
||||
.height(80)
|
||||
.centerX()
|
||||
shareBtn.layoutChain
|
||||
.left(30)
|
||||
.bottom(kSafeBottomMargin + 12)
|
||||
.height(56)
|
||||
.width(110)
|
||||
|
||||
groupNameLab.layoutChain
|
||||
.topToBottomOfView(groupIcon, offset: 4)
|
||||
.centerX()
|
||||
downloadBtn.layoutChain
|
||||
.leftToRightOfView(shareBtn, offset: 16)
|
||||
.right(30)
|
||||
.bottomToView(shareBtn)
|
||||
.heightToView(shareBtn)
|
||||
|
||||
shareAppBtn.layoutChain
|
||||
.left(44)
|
||||
.bottom(kSafeBottomMargin + 10)
|
||||
.widthToView(shareCodeBtn)
|
||||
.height(50)
|
||||
inviteCardView.layoutChain
|
||||
.topToBottomOfView(navView, offset: 42)
|
||||
.edgesHorzontal(15)
|
||||
.heightToWidth(481/345)
|
||||
|
||||
shareCodeBtn.layoutChain
|
||||
.leftToRightOfView(shareAppBtn, offset: 20)
|
||||
.right(44)
|
||||
.bottomToView(shareAppBtn)
|
||||
.widthToView(shareAppBtn)
|
||||
.heightToView(shareAppBtn)
|
||||
cardBgImg.layoutChain.edges()
|
||||
|
||||
inviteInfoView.layoutChain
|
||||
.topToBottomOfView(groupIcon, offset: 50)
|
||||
.edgesHorzontal(30)
|
||||
// .bottomToTopOfView(shareAppBtn, offset: -32)
|
||||
groupView.layoutChain
|
||||
.top(16)
|
||||
.edgesHorzontal(16)
|
||||
|
||||
inviteInfoTitleLab.layoutChain
|
||||
.top(20)
|
||||
.centerX()
|
||||
|
||||
inviteTipsLab.layoutChain
|
||||
.topToBottomOfView(inviteInfoTitleLab, offset: 9)
|
||||
.topToBottomOfView(groupIcon, offset: 28)
|
||||
.centerX()
|
||||
|
||||
inviteCodeView.layoutChain
|
||||
.topToBottomOfView(inviteTipsLab, offset: 10)
|
||||
.topToBottomOfView(inviteInfoTitleLab, offset: 12)
|
||||
.centerX()
|
||||
.width(180)
|
||||
.height(50)
|
||||
.width(200)
|
||||
.height(56)
|
||||
|
||||
inviteCodeTipsLab.layoutChain
|
||||
.top(10)
|
||||
.top(8)
|
||||
.centerX()
|
||||
|
||||
inviteCodeLab.layoutChain
|
||||
.topToBottomOfView(inviteCodeTipsLab)
|
||||
.topToBottomOfView(inviteCodeTipsLab, offset: 7)
|
||||
.centerX()
|
||||
|
||||
scanTipsLab.layoutChain
|
||||
.topToBottomOfView(inviteCodeView, offset: 20)
|
||||
inviteTipsLab.layoutChain
|
||||
.topToBottomOfView(inviteCodeView, offset: 10)
|
||||
.centerX()
|
||||
|
||||
qrCodeView.layoutChain
|
||||
.topToBottomOfView(scanTipsLab, offset: 15)
|
||||
.bottom(42)
|
||||
.centerX()
|
||||
.widthToHeight(1)
|
||||
.bottom(20)
|
||||
|
||||
qrCodeBgImg.layoutChain.edges()
|
||||
.width(138)
|
||||
.height(138)
|
||||
|
||||
qrCodeImgView.layoutChain
|
||||
.edgesHorzontal(4)
|
||||
.edgesVertical(4)
|
||||
}
|
||||
.edges(all: 18)
|
||||
|
||||
lazy var navBgView: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.image = UIImage(named: "Common/navBar_bg_2")
|
||||
iv.contentMode = .scaleAspectFill
|
||||
return iv
|
||||
}()
|
||||
scanTipsLab.layoutChain
|
||||
.bottomToTopOfView(qrCodeView, offset: -8)
|
||||
.centerX()
|
||||
}
|
||||
|
||||
lazy var navView: BaseNavigationView = {
|
||||
let nav = BaseNavigationView(title: "邀请成员")
|
||||
nav.backBtn.setImage(UIImage(named: "Group/invite_back"), for: .normal)
|
||||
nav.backBtn.layoutChain.width(36).height(36)
|
||||
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 = {
|
||||
let view = UIImageView()
|
||||
view.backgroundColor = .clear
|
||||
view.cornerRadius = 40
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.cornerRadius = 10
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var groupNameLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
label.textColor = UIColor(hexStr: "#293445")
|
||||
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var inviteInfoView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
|
||||
view.cornerRadius = 20
|
||||
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 groupDescLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var inviteInfoTitleLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "将此邀请码分享给加入圈子的人"
|
||||
label.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#353B4F")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var inviteTipsLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "此邀请码有效期为24小时"
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#16B3FF")
|
||||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
label.textColor = UIColor(hexStr: "#00ADFE")
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
@ -169,78 +235,80 @@ class InviteJoinView: UIView {
|
|||
view.borderColor = UIColor(hexStr: "#CCCCCC")
|
||||
view.borderWidth = 1
|
||||
view.cornerRadius = 10
|
||||
view.backgroundColor = .white
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var inviteCodeTipsLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "点击复制邀请码"
|
||||
label.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
label.textColor = ThemeManager.shared.color.contentColor
|
||||
label.font = .systemFont(ofSize: 11, weight: .medium)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var inviteCodeLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .heavy)
|
||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
label.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
label.textColor = UIColor(hexStr: "#00ADFE")
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var scanTipsLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "— 或扫描以下二维码 —"
|
||||
label.font = .systemFont(ofSize: 10)
|
||||
label.text = "扫描以下二维码"
|
||||
label.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
label.textColor = ThemeManager.shared.color.contentColor
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var qrCodeView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var qrCodeBgImg: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "Group/scan_bg")
|
||||
view.backgroundColor = .white
|
||||
view.borderWidth = 3
|
||||
view.borderColor = UIColor(hexStr: "#EAEEEF")
|
||||
view.cornerRadius = 34
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var qrCodeImgView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.backgroundColor = .clear
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.cornerRadius = 10
|
||||
view.backgroundColor = .white
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.cornerRadius = 8
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var shareAppBtn: UIButton = {
|
||||
lazy var shareBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("分享APP", for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
|
||||
btn.setTitle("分享", for: .normal)
|
||||
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||
btn.setTitleColor(UIColor(hexStr: "#00ADFE"), for: .normal)
|
||||
btn.backgroundColor = .white
|
||||
btn.borderWidth = 1
|
||||
btn.borderColor = UIColor(hexStr: "#16B3FF")
|
||||
btn.cornerRadius = 25
|
||||
btn.borderColor = UIColor(hexStr: "#00ADFE")
|
||||
btn.cornerRadius = 20
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var shareCodeBtn: UIButton = {
|
||||
lazy var downloadBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("分享邀请码", for: .normal)
|
||||
btn.setTitle("下载图片", 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.cornerRadius = 25
|
||||
btn.cornerRadius = 20
|
||||
btn.clipsToBounds = true
|
||||
return btn
|
||||
}()
|
||||
|
||||
/// 兼容旧引用:截图目标
|
||||
var inviteInfoView: UIView { inviteCardView }
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .white
|
||||
backgroundColor = .clear
|
||||
setupUI()
|
||||
setupRx()
|
||||
}
|
||||
|
|
@ -248,4 +316,29 @@ class InviteJoinView: UIView {
|
|||
required init?(coder aDecoder: NSCoder) {
|
||||
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 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? {
|
||||
didSet {
|
||||
guard let model = groupModel else { return }
|
||||
groupNameLab.text = model.groups.first(where: { $0.group_key == model.default_group_key })?.name ?? ""
|
||||
defaultGroupKey = model.default_group_key
|
||||
selectedGroupKey = model.default_group_key
|
||||
groupList = model.groups
|
||||
|
||||
let viewHeight = CGFloat(model.groups.prefix(5).count * 68)
|
||||
tableView.layoutChain.height(viewHeight, relation: .greaterThanOrEqual)
|
||||
updateTableHeight()
|
||||
}
|
||||
}
|
||||
|
||||
private var defaultGroupKey: String = ""
|
||||
private var selectedGroupKey: String = ""
|
||||
|
||||
private var groupList: [GroupInfoModel] = [] {
|
||||
didSet {
|
||||
|
|
@ -34,18 +38,16 @@ class GroupListPopView: UIView {
|
|||
/// 完成选中进行回调
|
||||
private var completion: ((String?) -> Void)?
|
||||
|
||||
@objc func tap() {
|
||||
@objc private func tapMask() {
|
||||
completion?(nil)
|
||||
}
|
||||
|
||||
@objc func createAction(button: UIButton) {
|
||||
@objc private func cancelAction() {
|
||||
completion?(nil)
|
||||
AppRouter.push(Route.createGroup)
|
||||
}
|
||||
|
||||
@objc func joinAction(button: UIButton) {
|
||||
completion?(nil)
|
||||
AppRouter.push(Route.joinGroup)
|
||||
@objc private func confirmAction() {
|
||||
completion?(selectedGroupKey.isEmpty ? nil : selectedGroupKey)
|
||||
}
|
||||
|
||||
private lazy var bgView: UIView = {
|
||||
|
|
@ -55,79 +57,63 @@ class GroupListPopView: UIView {
|
|||
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 = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
view.cornerRadius = 10
|
||||
view.backgroundColor = .clear
|
||||
|
||||
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
|
||||
}()
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .grouped)
|
||||
tableView.backgroundColor = .white
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.estimatedRowHeight = 68
|
||||
tableView.rowHeight = Self.rowHeight
|
||||
tableView.estimatedRowHeight = Self.rowHeight
|
||||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.bounces = false
|
||||
tableView.register(GroupListCell.self)
|
||||
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
return tableView
|
||||
}()
|
||||
|
||||
lazy var createGroupBtn: UIButton = {
|
||||
lazy var cancelBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("创建圈子", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
||||
btn.cornerRadius = 22
|
||||
btn.addTarget(self, action: #selector(createAction), for: .touchUpInside)
|
||||
btn.setTitle("取消", for: .normal)
|
||||
btn.setTitleColor(Self.primaryBlue, for: .normal)
|
||||
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||
btn.backgroundColor = .white
|
||||
btn.cornerRadius = 16
|
||||
btn.layer.borderWidth = 1
|
||||
btn.layer.borderColor = Self.primaryBlue.cgColor
|
||||
btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
lazy var joinGroupBtn: UIButton = {
|
||||
lazy var confirmBtn: UIButton = {
|
||||
let btn = UIButton(type: .custom)
|
||||
btn.setTitle("加入圈子", for: .normal)
|
||||
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
|
||||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
|
||||
btn.cornerRadius = 22
|
||||
btn.addTarget(self, action: #selector(joinAction), for: .touchUpInside)
|
||||
|
||||
btn.setTitle("确定", for: .normal)
|
||||
btn.setTitleColor(.white, for: .normal)
|
||||
btn.titleLabel?.font = FontManager.boboBold(18)
|
||||
btn.backgroundColor = Self.primaryBlue
|
||||
btn.cornerRadius = 16
|
||||
btn.addTarget(self, action: #selector(confirmAction), for: .touchUpInside)
|
||||
return btn
|
||||
}()
|
||||
|
||||
|
|
@ -137,61 +123,41 @@ class GroupListPopView: UIView {
|
|||
backgroundColor = .clear
|
||||
addSubview(bgView)
|
||||
bgView.addSubview(infoView)
|
||||
infoView.addSubview(headerImageView)
|
||||
infoView.addSubview(tableView)
|
||||
infoView.addSubview(createGroupBtn)
|
||||
infoView.addSubview(joinGroupBtn)
|
||||
infoView.addSubview(cancelBtn)
|
||||
infoView.addSubview(confirmBtn)
|
||||
|
||||
bgView.addSubview(groupView)
|
||||
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)
|
||||
bgView.layoutChain.edges()
|
||||
|
||||
infoView.layoutChain
|
||||
.topToBottomOfView(groupView, offset: 20)
|
||||
.edgesHorzontal(15)
|
||||
.bottom()
|
||||
.edgesHorzontal()
|
||||
|
||||
createGroupBtn.layoutChain
|
||||
.left(15)
|
||||
.bottom(20)
|
||||
.widthToView(joinGroupBtn)
|
||||
.height(44)
|
||||
headerImageView.layoutChain
|
||||
.top()
|
||||
.edgesHorzontal()
|
||||
.heightToWidth(127/375)
|
||||
|
||||
joinGroupBtn.layoutChain
|
||||
.leftToRightOfView(createGroupBtn, offset: 7)
|
||||
.right(15)
|
||||
.bottom(20)
|
||||
cancelBtn.layoutChain
|
||||
.left(20)
|
||||
.bottom(20 + kSafeBottomMargin)
|
||||
.height(44)
|
||||
.widthToView(confirmBtn)
|
||||
|
||||
confirmBtn.layoutChain
|
||||
.leftToRightOfView(cancelBtn, offset: 12)
|
||||
.right(20)
|
||||
.bottomToView(cancelBtn)
|
||||
.height(44)
|
||||
.widthToView(joinGroupBtn)
|
||||
|
||||
tableView.layoutChain
|
||||
.edges(excludingEdge: .bottom)
|
||||
.height(78, relation: .greaterThanOrEqual)
|
||||
.bottomToTopOfView(createGroupBtn, offset: -17)
|
||||
.topToBottomOfView(headerImageView, offset: -20)
|
||||
.edgesHorzontal(12)
|
||||
.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
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
|
@ -200,61 +166,66 @@ class GroupListPopView: UIView {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
private func updateTableHeight() {
|
||||
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
|
||||
extension GroupListPopView {
|
||||
|
||||
/// 显示选择弹窗
|
||||
/// - Parameters:
|
||||
/// - start: 显示起始点
|
||||
static func show(start: CGPoint,
|
||||
groupModel: GroupModel,
|
||||
/// 通过 window 从底部弹出圈子列表
|
||||
static func show(groupModel: GroupModel,
|
||||
completion: @escaping ((String?) -> Void)) {
|
||||
guard let superView = kKeyWindow else {
|
||||
return
|
||||
guard let superView = kKeyWindow else { return }
|
||||
|
||||
let pop = GroupListPopView.shared
|
||||
if pop.superview != nil {
|
||||
pop.removeFromSuperview()
|
||||
}
|
||||
|
||||
if GroupListPopView.shared.superview != nil {
|
||||
GroupListPopView.shared.removeFromSuperview()
|
||||
GroupListPopView.shared.bgView.frame = .zero
|
||||
}
|
||||
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
|
||||
pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
||||
pop.groupModel = groupModel
|
||||
pop.completion = { text in
|
||||
completion(text)
|
||||
GroupListPopView.dismiss()
|
||||
}
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
GroupListPopView.shared.infoView.alpha = 1
|
||||
// GroupListPopView.shared.infoView.dl.y = start.y
|
||||
|
||||
pop.bgView.alpha = 0
|
||||
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() {
|
||||
guard GroupListPopView.shared.superview != nil else { return }
|
||||
let viewHeight = GroupListPopView.shared.infoView.frame.height
|
||||
UIView.animate(withDuration: 0.15) {
|
||||
GroupListPopView.shared.infoView.alpha = 0
|
||||
// GroupListPopView.shared.infoView.dl.y = -viewHeight
|
||||
}
|
||||
let pop = GroupListPopView.shared
|
||||
guard pop.superview != nil else { return }
|
||||
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
|
||||
GroupListPopView.shared.removeFromSuperview()
|
||||
pop.infoView.transform = .identity
|
||||
pop.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
extension GroupListPopView: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
|
|
@ -273,105 +244,178 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
|
|||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.configure(model: groupList[indexPath.row],
|
||||
isSelected: defaultGroupKey == groupList[indexPath.row].group_key)
|
||||
let model = groupList[indexPath.row]
|
||||
cell.configure(model: model, isSelected: selectedGroupKey == model.group_key)
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
completion?(groupList[indexPath.row].group_key)
|
||||
selectedGroupKey = groupList[indexPath.row].group_key
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - GroupListPopCell
|
||||
// MARK: - GroupListCell
|
||||
class GroupListCell: UITableViewCell {
|
||||
|
||||
func configure(model: GroupInfoModel, isSelected: Bool) {
|
||||
avaterImgView.image = model.groupIcon
|
||||
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?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
setupSubviews()
|
||||
}
|
||||
|
||||
private func setupSubviews() {
|
||||
contentView.addSubview(bgView)
|
||||
contentView.addSubview(avaterImgView)
|
||||
contentView.addSubview(nameLab)
|
||||
contentView.addSubview(cardView)
|
||||
cardView.addSubview(avaterImgView)
|
||||
cardView.addSubview(nameLab)
|
||||
cardView.addSubview(tagStack)
|
||||
cardView.addSubview(countLab)
|
||||
cardView.addSubview(checkIcon)
|
||||
|
||||
bgView.layoutChain.edges()
|
||||
cardView.layoutChain
|
||||
.edgesVertical(4)
|
||||
.edgesHorzontal(4)
|
||||
|
||||
avaterImgView.layoutChain
|
||||
.edgesVertical(14)
|
||||
.left(15)
|
||||
.width(40)
|
||||
.height(40)
|
||||
.left(12)
|
||||
.centerY()
|
||||
.width(44)
|
||||
.height(44)
|
||||
|
||||
checkIcon.layoutChain
|
||||
.right(12)
|
||||
.centerY()
|
||||
.width(22)
|
||||
.height(22)
|
||||
|
||||
nameLab.layoutChain
|
||||
.centerY(avaterImgView)
|
||||
.leftToRightOfView(avaterImgView, offset: 11)
|
||||
.right(100, relation: .greaterThanOrEqual)
|
||||
.topToView(avaterImgView, offset: 2)
|
||||
.leftToRightOfView(avaterImgView, offset: 10)
|
||||
.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) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
|
||||
lazy var bgView: UIView = {
|
||||
lazy var cardView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
|
||||
|
||||
let lineView = UIView()
|
||||
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
|
||||
|
||||
view.backgroundColor = .white
|
||||
view.cornerRadius = 12
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var avaterImgView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
// view.backgroundColor = .lightGray
|
||||
view.contentMode = .scaleAspectFill
|
||||
// view.cornerRadius = 20
|
||||
view.cornerRadius = 8
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var nameLab: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = ThemeManager.shared.color.titleAuxColor
|
||||
label.textColor = UIColor(hexStr: "#353B4F")
|
||||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||||
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||