diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index c833cf6b..09eb7392 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -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 = ""; }; 305A75312FCA8C7000227D26 /* ImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlugin.swift; sourceTree = ""; }; 305A75322FCA8C7000227D26 /* NotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEmpty.swift; sourceTree = ""; }; - 305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "D-DIN-PRO-500-Medium.otf"; sourceTree = ""; }; 305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "zihunbiantaoti-r.ttf"; sourceTree = ""; }; 305A761A2FCA8C7000227D26 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; }; 305A761B2FCA8C7000227D26 /* Action+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+Internal.swift"; sourceTree = ""; }; @@ -397,6 +399,7 @@ 305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableType+ObjectMapper.swift"; sourceTree = ""; }; 305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+ObjectMapper.swift"; sourceTree = ""; }; 305A76232FCA8C7000227D26 /* GroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupView.swift; sourceTree = ""; }; + 55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = ""; }; 305A76242FCA8C7000227D26 /* GroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewController.swift; sourceTree = ""; }; 305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = ""; }; 305A76272FCA8C7000227D26 /* GroupMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView.swift; sourceTree = ""; }; @@ -605,6 +608,9 @@ 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = ""; }; 55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = ""; }; 55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = ""; }; + 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = ""; }; + 55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = ""; }; + 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = ""; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = ""; }; 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = ""; }; A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -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 = ""; }; + 55B217A23022F0A000784703 /* Font */ = { + isa = PBXGroup; + children = ( + 55B217A03022F0A000784701 /* FontManager.swift */, + ); + path = Font; + sourceTree = ""; + }; 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 */, diff --git a/QuickLocation/AppDelegate.swift b/QuickLocation/AppDelegate.swift index ff9870e8..c57ea5dd 100644 --- a/QuickLocation/AppDelegate.swift +++ b/QuickLocation/AppDelegate.swift @@ -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() diff --git a/QuickLocation/Assets.xcassets/Group/invite_back.imageset/Contents.json b/QuickLocation/Assets.xcassets/Group/invite_back.imageset/Contents.json new file mode 100644 index 00000000..79099b29 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Group/invite_back.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Group/invite_back.imageset/back@2x.png b/QuickLocation/Assets.xcassets/Group/invite_back.imageset/back@2x.png new file mode 100644 index 00000000..85681381 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/invite_back.imageset/back@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/invite_back.imageset/back@3x.png b/QuickLocation/Assets.xcassets/Group/invite_back.imageset/back@3x.png new file mode 100644 index 00000000..55442614 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/invite_back.imageset/back@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/Contents.json new file mode 100644 index 00000000..03938de7 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@2x.png b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@2x.png new file mode 100644 index 00000000..e1651913 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@3x.png b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@3x.png new file mode 100644 index 00000000..bf7d30fb Binary files /dev/null and b/QuickLocation/Assets.xcassets/Group/invite_card_bg.imageset/组 47999@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png index 6d2b07e1..cdb3bf04 100644 Binary files a/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png and b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png index 5a14cdc9..c27bd7fb 100644 Binary files a/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png and b/QuickLocation/Assets.xcassets/GroupMemberList/selected_bg.imageset/Union@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/Contents.json new file mode 100644 index 00000000..8fca44c6 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/组 47986@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/组 47986@2x.png new file mode 100644 index 00000000..dc3306fa Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/组 47986@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/组 47986@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/组 47986@3x.png new file mode 100644 index 00000000..0aff8b7a Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/sos.imageset/组 47986@3x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/Contents.json b/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/Contents.json new file mode 100644 index 00000000..d10f4289 --- /dev/null +++ b/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@2x.png b/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@2x.png new file mode 100644 index 00000000..7bf141ea Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@2x.png differ diff --git a/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@3x.png b/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@3x.png new file mode 100644 index 00000000..605192b4 Binary files /dev/null and b/QuickLocation/Assets.xcassets/GroupMemberList/vip.imageset/蒙版组 13899@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/Contents.json new file mode 100644 index 00000000..484a5702 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@2x.png b/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@2x.png new file mode 100644 index 00000000..451ae031 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@3x.png b/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@3x.png new file mode 100644 index 00000000..38d3836f Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/group_list_header.imageset/组 47987@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@2x.png b/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@2x.png index 215c4e05..c94805a7 100644 Binary files a/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@2x.png and b/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@3x.png b/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@3x.png index 7eedf54c..c3f7eac9 100644 Binary files a/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@3x.png and b/QuickLocation/Assets.xcassets/Home/location.imageset/Group_50@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_battery.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_battery.imageset/Contents.json new file mode 100644 index 00000000..f3d8aa31 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_battery.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_battery.imageset/组 47777@2x.png b/QuickLocation/Assets.xcassets/Home/member_battery.imageset/组 47777@2x.png new file mode 100644 index 00000000..c03967a5 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_battery.imageset/组 47777@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_battery.imageset/组 47777@3x.png b/QuickLocation/Assets.xcassets/Home/member_battery.imageset/组 47777@3x.png new file mode 100644 index 00000000..c9e8f260 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_battery.imageset/组 47777@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/Contents.json new file mode 100644 index 00000000..f57170e5 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@2x.png b/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@2x.png new file mode 100644 index 00000000..a5354adf Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@3x.png b/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@3x.png new file mode 100644 index 00000000..c6c19312 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_battery_low.imageset/组 47778@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/Contents.json new file mode 100644 index 00000000..26c1b40e --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@2x.png b/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@2x.png new file mode 100644 index 00000000..dbf67fa8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@3x.png b/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@3x.png new file mode 100644 index 00000000..244094d4 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_battery_unknown.imageset/组 47776@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_clock.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_clock.imageset/Contents.json new file mode 100644 index 00000000..4231c99e --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_clock.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_clock.imageset/clock@2x.png b/QuickLocation/Assets.xcassets/Home/member_clock.imageset/clock@2x.png new file mode 100644 index 00000000..dd501f7a Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_clock.imageset/clock@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_clock.imageset/clock@3x.png b/QuickLocation/Assets.xcassets/Home/member_clock.imageset/clock@3x.png new file mode 100644 index 00000000..e3b2bd28 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_clock.imageset/clock@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_invite.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_invite.imageset/Contents.json new file mode 100644 index 00000000..ccf5f63f --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_invite.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@2x.png b/QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@2x.png new file mode 100644 index 00000000..c502b595 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@3x.png b/QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@3x.png new file mode 100644 index 00000000..12277bef Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_invite.imageset/member_invite@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@2x.png b/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@2x.png index b9913f37..e81a2a13 100644 Binary files a/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@2x.png and b/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@3x.png b/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@3x.png index bd7c8ad8..2ac1de27 100644 Binary files a/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@3x.png and b/QuickLocation/Assets.xcassets/Home/member_location.imageset/Group_1565@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_logo.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_logo.imageset/Contents.json new file mode 100644 index 00000000..b0e7b2cc --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_logo.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_logo.imageset/组 47725@2x.png b/QuickLocation/Assets.xcassets/Home/member_logo.imageset/组 47725@2x.png new file mode 100644 index 00000000..6d4c1bc0 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_logo.imageset/组 47725@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_logo.imageset/组 47725@3x.png b/QuickLocation/Assets.xcassets/Home/member_logo.imageset/组 47725@3x.png new file mode 100644 index 00000000..0506120d Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_logo.imageset/组 47725@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_map.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_map.imageset/Contents.json new file mode 100644 index 00000000..bf75c752 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_map.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@2x.png b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@2x.png new file mode 100644 index 00000000..1a0ca9bd Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@3x.png b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@3x.png new file mode 100644 index 00000000..bfa643dc Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_map.imageset/组 48007@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/Contents.json new file mode 100644 index 00000000..65aa0f30 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@2x.png b/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@2x.png new file mode 100644 index 00000000..0348a0e6 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@3x.png b/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@3x.png new file mode 100644 index 00000000..525cde65 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_network_unknown.imageset/蒙版组 13863@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_phone.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_phone.imageset/Contents.json new file mode 100644 index 00000000..e273d927 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_phone.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_phone.imageset/蒙版组 13862@2x.png b/QuickLocation/Assets.xcassets/Home/member_phone.imageset/蒙版组 13862@2x.png new file mode 100644 index 00000000..422fcec5 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_phone.imageset/蒙版组 13862@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_phone.imageset/蒙版组 13862@3x.png b/QuickLocation/Assets.xcassets/Home/member_phone.imageset/蒙版组 13862@3x.png new file mode 100644 index 00000000..3b89cb54 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_phone.imageset/蒙版组 13862@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/Contents.json new file mode 100644 index 00000000..e273d927 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@2x.png b/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@2x.png new file mode 100644 index 00000000..ed9437d3 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@3x.png b/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@3x.png new file mode 100644 index 00000000..1bf12ff5 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_phone_unknown.imageset/蒙版组 13862@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/Contents.json new file mode 100644 index 00000000..adfee5fa --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@2x.png b/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@2x.png new file mode 100644 index 00000000..4eb8104e Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@3x.png b/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@3x.png new file mode 100644 index 00000000..2291434a Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_today_track.imageset/today_track@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/Contents.json new file mode 100644 index 00000000..4445f6af --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@2x.png b/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@2x.png new file mode 100644 index 00000000..e90248e4 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@3x.png b/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@3x.png new file mode 100644 index 00000000..f3f0c2ad Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_weather_unknown.imageset/蒙版组 13878@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/Contents.json b/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/Contents.json new file mode 100644 index 00000000..65aa0f30 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/蒙版组 13863@2x.png b/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/蒙版组 13863@2x.png new file mode 100644 index 00000000..eccac7af Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/蒙版组 13863@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/蒙版组 13863@3x.png b/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/蒙版组 13863@3x.png new file mode 100644 index 00000000..079e0c78 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Home/member_wifi.imageset/蒙版组 13863@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png index 0e1988fb..496aa63d 100644 Binary files a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png and b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png index dabfdc12..80d8bff2 100644 Binary files a/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png and b/QuickLocation/Assets.xcassets/Home/warning.imageset/Group_2220@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Map/arrow.imageset/Contents.json b/QuickLocation/Assets.xcassets/Map/arrow.imageset/Contents.json new file mode 100644 index 00000000..60f1171d --- /dev/null +++ b/QuickLocation/Assets.xcassets/Map/arrow.imageset/Contents.json @@ -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 + } +} diff --git a/QuickLocation/Assets.xcassets/Map/arrow.imageset/蒙版组 13900@2x.png b/QuickLocation/Assets.xcassets/Map/arrow.imageset/蒙版组 13900@2x.png new file mode 100644 index 00000000..8f9d8fe9 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Map/arrow.imageset/蒙版组 13900@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Map/arrow.imageset/蒙版组 13900@3x.png b/QuickLocation/Assets.xcassets/Map/arrow.imageset/蒙版组 13900@3x.png new file mode 100644 index 00000000..4037e6a8 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Map/arrow.imageset/蒙版组 13900@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@2x.png b/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@2x.png index 984fd2bd..68e82706 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@3x.png b/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@3x.png index f56305e5..de947d57 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/explore.imageset/explore@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@2x.png b/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@2x.png index 562024e4..412bbf5e 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@3x.png b/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@3x.png index 45a8f3d9..a765522a 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/explore_selected.imageset/explore_selected@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@2x.png b/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@2x.png index 817fbd51..ee048678 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@3x.png b/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@3x.png index 87c6681a..935282c7 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/group.imageset/group@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@2x.png b/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@2x.png index 2ed20ea6..bfdc76bc 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@3x.png b/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@3x.png index 77ae664d..661d4910 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/group_selected.imageset/group_selected@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@2x.png b/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@2x.png index 03f5b417..e7ae0c95 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@3x.png b/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@3x.png index ef69c50a..d8d0fb6f 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/location.imageset/location@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@2x.png b/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@2x.png index 4775609f..3fef8c0a 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@3x.png b/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@3x.png index 94ac075a..656f09d2 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/location_selected.imageset/location_selected@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@2x.png b/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@2x.png index d5aae932..1a6bd3a1 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@3x.png b/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@3x.png index 50f6ebbd..2c80a057 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/mine.imageset/mine@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@2x.png b/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@2x.png index 171ab35e..a6498a20 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@2x.png and b/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@3x.png b/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@3x.png index bc0dd76c..dbc57faf 100644 Binary files a/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@3x.png and b/QuickLocation/Assets.xcassets/Tabbar/mine_selected.imageset/mine_selected@3x.png differ diff --git a/QuickLocation/Core/Extension/UIFont+Extension.swift b/QuickLocation/Core/Extension/UIFont+Extension.swift index f0c595cb..db4de634 100644 --- a/QuickLocation/Core/Extension/UIFont+Extension.swift +++ b/QuickLocation/Core/Extension/UIFont+Extension.swift @@ -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? + 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 { // diff --git a/QuickLocation/Core/Font/FontManager.swift b/QuickLocation/Core/Font/FontManager.swift new file mode 100644 index 00000000..7315e757 --- /dev/null +++ b/QuickLocation/Core/Font/FontManager.swift @@ -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 + } +} diff --git a/QuickLocation/Info.plist b/QuickLocation/Info.plist index 7e4c3f0d..3b6e95c8 100644 --- a/QuickLocation/Info.plist +++ b/QuickLocation/Info.plist @@ -66,7 +66,8 @@ UIAppFonts - douyu.otf + 荆南波波黑-Bold.ttf + 优设标题黑_猫啃网.ttf zihunbiantaoti-r.ttf UIBackgroundModes diff --git a/QuickLocation/Main/Tabbar/MainTabBarController.swift b/QuickLocation/Main/Tabbar/MainTabBarController.swift index 40beb5fe..07940d85 100644 --- a/QuickLocation/Main/Tabbar/MainTabBarController.swift +++ b/QuickLocation/Main/Tabbar/MainTabBarController.swift @@ -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"), diff --git a/QuickLocation/Main/Tabbar/QuickLocationTabBar.swift b/QuickLocation/Main/Tabbar/QuickLocationTabBar.swift index 3d0fdd6f..29b02819 100644 --- a/QuickLocation/Main/Tabbar/QuickLocationTabBar.swift +++ b/QuickLocation/Main/Tabbar/QuickLocationTabBar.swift @@ -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 diff --git a/QuickLocation/Manager/Unlock/UnlockCountManager.swift b/QuickLocation/Manager/Unlock/UnlockCountManager.swift new file mode 100644 index 00000000..719939aa --- /dev/null +++ b/QuickLocation/Manager/Unlock/UnlockCountManager.swift @@ -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)") + } +} diff --git a/QuickLocation/Resource/Font/D-DIN-PRO-500-Medium.otf b/QuickLocation/Resource/Font/D-DIN-PRO-500-Medium.otf deleted file mode 100644 index d72d454e..00000000 Binary files a/QuickLocation/Resource/Font/D-DIN-PRO-500-Medium.otf and /dev/null differ diff --git a/QuickLocation/Resource/Font/优设标题黑_猫啃网.ttf b/QuickLocation/Resource/Font/优设标题黑_猫啃网.ttf new file mode 100644 index 00000000..3729151a Binary files /dev/null and b/QuickLocation/Resource/Font/优设标题黑_猫啃网.ttf differ diff --git a/QuickLocation/Resource/Font/荆南波波黑-Bold.ttf b/QuickLocation/Resource/Font/荆南波波黑-Bold.ttf new file mode 100644 index 00000000..5e59e8a6 Binary files /dev/null and b/QuickLocation/Resource/Font/荆南波波黑-Bold.ttf differ diff --git a/QuickLocation/Section/Group/GroupItineraryView.swift b/QuickLocation/Section/Group/GroupItineraryView.swift new file mode 100644 index 00000000..43d89ff3 --- /dev/null +++ b/QuickLocation/Section/Group/GroupItineraryView.swift @@ -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) + } +} diff --git a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift index ac680b87..b03ed8b3 100644 --- a/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift +++ b/QuickLocation/Section/Group/GroupMemberList/GroupMemberListView.swift @@ -649,13 +649,17 @@ 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 @@ -665,11 +669,37 @@ class GroupMemberListCell: UICollectionViewCell { // batteryLab.text = "\(model.battery)%" // } } + + 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 diff --git a/QuickLocation/Section/Group/GroupView.swift b/QuickLocation/Section/Group/GroupView.swift index 816fce69..e4e847b6 100644 --- a/QuickLocation/Section/Group/GroupView.swift +++ b/QuickLocation/Section/Group/GroupView.swift @@ -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,12 +256,44 @@ 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 }() - + lazy var scanBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Group/scan"), for: .normal) @@ -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 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 createGroupBtn: UIView = { + let v = UIView() + v.isUserInteractionEnabled = true + return v }() - // MARK: - 热门酷圈 + lazy var joinGroupBtn: UIView = { + let v = UIView() + v.isUserInteractionEnabled = true + return v + }() + + // MARK: - 热门酷圈(隐藏保留绑定) private lazy var hotGroupTitleLabel: UILabel = { let label = UILabel() label.text = "热门酷圈" @@ -255,37 +386,46 @@ class GroupView: UIView { return cv }() - // MARK: - Segment - private(set) lazy var segmentView: UIView = { + // MARK: - List container + private lazy var listContainer: 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) - return v }() - private(set) lazy var createdTabLabel: UILabel = { + // 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) + + return v + }() + + /// 「全部圈子」→ 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() } @@ -514,7 +669,7 @@ final class CircleGroupCell: UITableViewCell { view.isHidden = true return view }() - + private let unreadBadge: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 10, weight: .medium) @@ -565,11 +720,11 @@ final class CircleGroupCell: UITableViewCell { unreadBadgeView.layoutChain .centerY(nameLabel) .right(15) - + unreadBadge.layoutChain .edgesHorzontal(8) .edgesVertical(1) - + timeLabel.layoutChain .right(15) .centerY(countLabel) @@ -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)个成员" diff --git a/QuickLocation/Section/Group/GroupViewController.swift b/QuickLocation/Section/Group/GroupViewController.swift index 07575857..c3cd9d5e 100644 --- a/QuickLocation/Section/Group/GroupViewController.swift +++ b/QuickLocation/Section/Group/GroupViewController.swift @@ -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() @@ -104,7 +110,7 @@ final class GroupViewController: BaseViewController { .observe(on: MainScheduler.asyncInstance) .bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource)) .disposed(by: disposeBag) - + rootView.hotGroupsCollectionView.rx.modelSelected(GroupInfoModel.self) .subscribe(viewModel.hotGroupCellAction.inputs) .disposed(by: disposeBag) @@ -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() @@ -140,7 +162,7 @@ final class GroupViewController: BaseViewController { AppRouter.push(Route.groupChat, userInfo: ["groupId": groupId]) }) .disposed(by: disposeBag) - + // MQTT join/leave/dismiss 后刷新群列表 NotificationCenter.default.rx.notification(.RefreshIMGroupListNotification) .subscribe(onNext: { [weak self] _ in @@ -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 @@ -215,7 +315,7 @@ final class GroupViewController: BaseViewController { self.viewModel.loadHotGroupData(response.list) }).disposed(by: disposeBag) } - + private func requestGroupInfo() { GroupService.groupInfo().subscribe { response in guard let model = response.model else { return } diff --git a/QuickLocation/Section/Group/GroupViewModel.swift b/QuickLocation/Section/Group/GroupViewModel.swift index 20682c35..bf94a550 100644 --- a/QuickLocation/Section/Group/GroupViewModel.swift +++ b/QuickLocation/Section/Group/GroupViewModel.swift @@ -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 diff --git a/QuickLocation/Section/Group/InviteJoin/InviteJoinVC.swift b/QuickLocation/Section/Group/InviteJoin/InviteJoinVC.swift index 142a7136..e7be01c7 100644 --- a/QuickLocation/Section/Group/InviteJoin/InviteJoinVC.swift +++ b/QuickLocation/Section/Group/InviteJoin/InviteJoinVC.swift @@ -10,6 +10,7 @@ import ObjectMapper import RxSwift import RxCocoa import Kingfisher +import Photos class InviteJoinVC: BaseViewController { @@ -19,92 +20,144 @@ class InviteJoinVC: BaseViewController { rootView = InviteJoinView(frame: UIScreen.main.bounds) view = rootView } - + private let groupInModel: GroupInfoModel? - + 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 } - guard let config = AppContextManager.shared.systemConfig, - let shareConfig = config.shareConfig else { return } - - DLToast.showLoading() - - let fallbackImage = UIImage(named: "Common/logo108") - let imageURL: URL? = { - if shareConfig.image.isEmpty { return nil } - return URL(string: shareConfig.image) - }() - - let sendMessage: (UIImage?) -> Void = { [weak self] thumbImage in - guard let self = self else { return } - DLToast.dismiss() - - guard let screenshot = self.rootView.inviteInfoView.takeScreenshot(), - let imageData = screenshot.pngData() else { - DLToast.showError(text: "分享失败") - return - } - - let message = WXMediaMessage() - message.title = "邀请加入圈子" - message.description = shareConfig.content - if let thumb = thumbImage { - message.setThumbImage(thumb) - } - - let imageObject = WXImageObject() - imageObject.imageData = imageData - message.mediaObject = imageObject - - let req = SendMessageToWXReq() - req.bText = false - req.message = message - req.scene = 0 - - WXApi.send(req) - } - - if let url = imageURL { - KingfisherManager.shared.retrieveImage(with: url) { result in - switch result { - case .success(let value): - sendMessage(value.image) - case .failure: - sendMessage(fallbackImage) - } - } - } else { - sendMessage(fallbackImage) - } + // 下载图片到相册 + rootView.downloadBtn.rx.tap.subscribe(onNext: { [weak self] in + self?.downloadInviteCard() }).disposed(by: disposeBag) - + guard let model = groupInModel else { return } - rootView.groupIcon.image = model.groupIcon - rootView.groupNameLab.text = model.name + 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 } + + DLToast.showLoading() + + let fallbackImage = UIImage(named: "Common/logo108") + let imageURL: URL? = { + if shareConfig.image.isEmpty { return nil } + return URL(string: shareConfig.image) + }() + + let sendMessage: (UIImage?) -> Void = { [weak self] thumbImage in + guard let self = self else { return } + DLToast.dismiss() + + guard let screenshot = self.rootView.inviteCardView.takeScreenshot(), + let imageData = screenshot.pngData() else { + DLToast.showError(text: "分享失败") + return + } + + let message = WXMediaMessage() + message.title = "邀请加入圈子" + message.description = shareConfig.content + if let thumb = thumbImage { + message.setThumbImage(thumb) + } + + let imageObject = WXImageObject() + imageObject.imageData = imageData + message.mediaObject = imageObject + + let req = SendMessageToWXReq() + req.bText = false + req.message = message + req.scene = 0 + + WXApi.send(req) + } + + if let url = imageURL { + KingfisherManager.shared.retrieveImage(with: url) { result in + switch result { + case .success(let value): + sendMessage(value.image) + case .failure: + sendMessage(fallbackImage) + } + } + } else { + sendMessage(fallbackImage) + } + } + + 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() } @@ -116,9 +169,8 @@ class InviteJoinVC: BaseViewController { self.groupInModel = GroupInfoModel.init(JSON: groupInfo) super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - } diff --git a/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift b/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift index 8b67a964..53483a0b 100644 --- a/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift +++ b/QuickLocation/Section/Group/InviteJoin/InviteJoinView.swift @@ -12,235 +12,303 @@ import RxCocoa class InviteJoinView: UIView { var disposeBag = DisposeBag() - + + func configure(model: GroupInfoModel) { + groupIcon.image = model.groupIcon + groupNameLab.text = model.name + groupDescLab.text = model.description + groupDescLab.isHidden = model.description.isEmpty + + 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 { _ in - guard let code = self.inviteCodeLab.text else { return } + 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() - - groupNameLab.layoutChain - .topToBottomOfView(groupIcon, offset: 4) - .centerX() - - shareAppBtn.layoutChain - .left(44) - .bottom(kSafeBottomMargin + 10) - .widthToView(shareCodeBtn) - .height(50) - - shareCodeBtn.layoutChain - .leftToRightOfView(shareAppBtn, offset: 20) - .right(44) - .bottomToView(shareAppBtn) - .widthToView(shareAppBtn) - .heightToView(shareAppBtn) - - inviteInfoView.layoutChain - .topToBottomOfView(groupIcon, offset: 50) - .edgesHorzontal(30) -// .bottomToTopOfView(shareAppBtn, offset: -32) + + shareBtn.layoutChain + .left(30) + .bottom(kSafeBottomMargin + 12) + .height(56) + .width(110) + + downloadBtn.layoutChain + .leftToRightOfView(shareBtn, offset: 16) + .right(30) + .bottomToView(shareBtn) + .heightToView(shareBtn) + + inviteCardView.layoutChain + .topToBottomOfView(navView, offset: 42) + .edgesHorzontal(15) + .heightToWidth(481/345) + + cardBgImg.layoutChain.edges() + + groupView.layoutChain + .top(16) + .edgesHorzontal(16) inviteInfoTitleLab.layoutChain - .top(20) + .topToBottomOfView(groupIcon, offset: 28) .centerX() - - inviteTipsLab.layoutChain - .topToBottomOfView(inviteInfoTitleLab, offset: 9) - .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() + + inviteTipsLab.layoutChain + .topToBottomOfView(inviteCodeView, offset: 10) + .centerX() + + qrCodeView.layoutChain + .bottom(42) + .centerX() + .width(138) + .height(138) + + qrCodeImgView.layoutChain + .edges(all: 18) scanTipsLab.layoutChain - .topToBottomOfView(inviteCodeView, offset: 20) + .bottomToTopOfView(qrCodeView, offset: -8) .centerX() - - qrCodeView.layoutChain - .topToBottomOfView(scanTipsLab, offset: 15) - .centerX() - .widthToHeight(1) - .bottom(20) - - qrCodeBgImg.layoutChain.edges() - - qrCodeImgView.layoutChain - .edgesHorzontal(4) - .edgesVertical(4) } - - lazy var navBgView: UIImageView = { - let iv = UIImageView() - iv.image = UIImage(named: "Common/navBar_bg_2") - iv.contentMode = .scaleAspectFill - return iv - }() - + 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 - return view - }() - - lazy var groupNameLab: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 16, weight: .medium) - label.textColor = ThemeManager.shared.color.titleAuxColor - return label - }() - - lazy var inviteInfoView: UIView = { - let view = UIView() - view.backgroundColor = UIColor(hexStr: "#F5FBFF") - view.cornerRadius = 20 - return view - }() - - lazy var inviteInfoTitleLab: UILabel = { - let label = UILabel() - label.text = "将此邀请码分享给加入圈子的人" - label.font = .systemFont(ofSize: 16, weight: .medium) - label.textColor = ThemeManager.shared.color.titleAuxColor - return label - }() - - lazy var inviteTipsLab: UILabel = { - let label = UILabel() - label.text = "此邀请码有效期为24小时" - label.font = .systemFont(ofSize: 12, weight: .medium) - label.textColor = UIColor(hexStr: "#16B3FF") - return label - }() - - lazy var inviteCodeView: UIView = { - let view = UIView() - view.borderColor = UIColor(hexStr: "#CCCCCC") - view.borderWidth = 1 - view.cornerRadius = 10 - return view - }() - - lazy var inviteCodeTipsLab: UILabel = { - let label = UILabel() - label.text = "点击复制邀请码" - label.font = .systemFont(ofSize: 12, weight: .medium) - label.textColor = ThemeManager.shared.color.contentColor - return label - }() - - lazy var inviteCodeLab: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 16, weight: .heavy) - label.textColor = ThemeManager.shared.color.titleAuxColor - return label - }() - - lazy var scanTipsLab: UILabel = { - let label = UILabel() - label.text = "— 或扫描以下二维码 —" - label.font = .systemFont(ofSize: 10) - 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") - return view - }() - - lazy var qrCodeImgView: UIImageView = { - let view = UIImageView() - view.backgroundColor = .clear view.contentMode = .scaleAspectFill view.cornerRadius = 10 view.clipsToBounds = true return view }() - - lazy var shareAppBtn: UIButton = { + + lazy var groupNameLab: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 16, weight: .bold) + label.textColor = UIColor(hexStr: "#293445") + 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 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: 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: .regular) + label.textColor = UIColor(hexStr: "#00ADFE") + return label + }() + + lazy var inviteCodeView: UIView = { + let view = 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: 11, weight: .medium) + label.textColor = UIColor(hexStr: "#999999") + return label + }() + + lazy var inviteCodeLab: UILabel = { + let label = UILabel() + 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: 12, weight: .regular) + label.textColor = ThemeManager.shared.color.contentColor + return label + }() + + lazy var qrCodeView: UIView = { + let view = UIView() + 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 = .white + view.contentMode = .scaleAspectFit + view.cornerRadius = 8 + view.clipsToBounds = true + return view + }() + + 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 - return btn - }() - - lazy var shareCodeBtn: UIButton = { - let btn = UIButton(type: .custom) - btn.setTitle("分享邀请码", for: .normal) - btn.setTitleColor(.white, for: .normal) - btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) - btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) - btn.cornerRadius = 25 + btn.borderColor = UIColor(hexStr: "#00ADFE") + btn.cornerRadius = 20 return btn }() + lazy var downloadBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setTitle("下载图片", for: .normal) + btn.setTitleColor(.white, for: .normal) + btn.titleLabel?.font = FontManager.boboBold(18) + btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) + 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) + } } diff --git a/QuickLocation/Section/Home/GroupListPopView.swift b/QuickLocation/Section/Home/GroupListPopView.swift index 2604102d..fa7806e6 100644 --- a/QuickLocation/Section/Home/GroupListPopView.swift +++ b/QuickLocation/Section/Home/GroupListPopView.swift @@ -11,250 +11,221 @@ 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 { tableView.reloadData() } } - + /// 完成选中进行回调 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 = { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.5) view.clipsToBounds = true 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 }() - + // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear addSubview(bgView) bgView.addSubview(infoView) + infoView.addSubview(headerImageView) infoView.addSubview(tableView) - infoView.addSubview(createGroupBtn) - infoView.addSubview(joinGroupBtn) - - bgView.addSubview(groupView) - groupView.addSubview(groupIconView) - groupView.addSubview(groupNameLab) - groupView.addSubview(groupArrowIconView) - - groupView.layoutChain - .top(59) - .height(36) - .centerX() - .width(185, relation: .greaterThanOrEqual) + infoView.addSubview(cancelBtn) + infoView.addSubview(confirmBtn) - groupIconView.layoutChain - .left(11) - .centerY() - .width(30) - .height(30) + bgView.layoutChain.edges() - groupArrowIconView.layoutChain - .right(15) - .centerY() - .width(15) - .height(8.5) - - groupNameLab.layoutChain - .edgesVertical() - .leftToRightOfView(groupIconView) - .rightToLeftOfView(groupArrowIconView) - infoView.layoutChain - .topToBottomOfView(groupView, offset: 20) - .edgesHorzontal(15) - - createGroupBtn.layoutChain - .left(15) - .bottom(20) - .widthToView(joinGroupBtn) + .bottom() + .edgesHorzontal() + + headerImageView.layoutChain + .top() + .edgesHorzontal() + .heightToWidth(127/375) + + cancelBtn.layoutChain + .left(20) + .bottom(20 + kSafeBottomMargin) .height(44) - - joinGroupBtn.layoutChain - .leftToRightOfView(createGroupBtn, offset: 7) - .right(15) - .bottom(20) + .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) - - let tap = UITapGestureRecognizer(target: self, action: #selector(tap)) + .topToBottomOfView(headerImageView, offset: -20) + .edgesHorzontal(12) + .height(Self.rowHeight) + .bottomToTopOfView(cancelBtn, offset: -16) + + let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask)) tap.delegate = self addGestureRecognizer(tap) } - + required init?(coder aDecoder: NSCoder) { 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 { @@ -270,108 +241,181 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { groupList.count } - + 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) - - bgView.layoutChain.edges() - + contentView.addSubview(cardView) + cardView.addSubview(avaterImgView) + cardView.addSubview(nameLab) + cardView.addSubview(tagStack) + cardView.addSubview(countLab) + cardView.addSubview(checkIcon) + + 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) + } } diff --git a/QuickLocation/Section/Home/GroupMemberView2.swift b/QuickLocation/Section/Home/GroupMemberView2.swift index ddfec15d..6c14eb02 100644 --- a/QuickLocation/Section/Home/GroupMemberView2.swift +++ b/QuickLocation/Section/Home/GroupMemberView2.swift @@ -6,13 +6,22 @@ // import UIKit +import Network +import CoreLocation import RxSwift import RxCocoa +import SwiftyUserDefaults class GroupMemberView2: UIView { private var navBarHeightConstraint: NSLayoutConstraint? + private static let batteryNormalColor = UIColor(hexStr: "#47DD00") + private static let batteryLowColor = UIColor(hexStr: "#F85A5D") + private static let batteryDefaultTextColor = UIColor(hexStr: "#353B4F") + /// 24pt 图标内腔可用填充宽度(去掉描边与右侧电极) + private static let batteryFillMaxWidth: CGFloat = 18 + /// 最大化时展开导航占位为 kNaviHeight,收起/dismiss 时为 0 func setNavBarExpanded(_ expanded: Bool) { navBarHeightConstraint?.constant = expanded ? kNaviHeight : 0 @@ -24,7 +33,185 @@ class GroupMemberView2: UIView { statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8") statusLab.text = model.is_online ? "在线" : "离线" locationLab.text = model.lastLocation + + let isCurrentUser = model.user_id == AppContextManager.shared.userId + updateDistance(for: model, isCurrentUser: isCurrentUser) + + if isCurrentUser { + setupCurrentUserDeviceInfo() + } else { + setupUnknownDeviceInfo(battery: model.battery) + } } + + private func updateDistance(for model: GroupMemberModel, isCurrentUser: Bool) { + guard !isCurrentUser else { + distanceView.isHidden = true + return + } + guard let lat = Defaults[\.currentLatitude], + let lon = Defaults[\.currentLongitude] else { + distanceView.isHidden = true + return + } + let meCoord = CLLocationCoordinate2D(latitude: lat, longitude: lon) + let (otherCoord, _) = CircleMember.parsePosition(model.last_position) + guard CLLocationCoordinate2DIsValid(meCoord), + CLLocationCoordinate2DIsValid(otherCoord) else { + distanceView.isHidden = true + return + } + let meters = CLLocation(latitude: meCoord.latitude, longitude: meCoord.longitude) + .distance(from: CLLocation(latitude: otherCoord.latitude, longitude: otherCoord.longitude)) + distanceLab.text = Self.formatDistance(meters) + distanceView.isHidden = false + } + + private static func formatDistance(_ meters: CLLocationDistance) -> String { + if meters < 1000 { + return "\(Int(meters.rounded()))M" + } + return String(format: "%.1fKM", meters / 1000.0) + } + + private func setupCurrentUserDeviceInfo() { + phoneModelIcon.image = UIImage(named: "Home/member_phone") + phoneModelLab.text = Self.localPhoneModelName() + phoneModelLab.textColor = Self.batteryDefaultTextColor + + networkIcon.image = UIImage(named: "Home/member_wifi") + networkLab.text = Self.localNetworkStatusText() + networkLab.textColor = Self.batteryDefaultTextColor + + applyBatteryDisplay(percent: Self.localBatteryPercent()) + } + + private func setupUnknownDeviceInfo(battery: String) { + phoneModelIcon.image = UIImage(named: "Home/member_phone_unknown") + phoneModelLab.text = "***" + phoneModelLab.textColor = Self.batteryDefaultTextColor + + networkIcon.image = UIImage(named: "Home/member_network_unknown") + networkLab.text = "***" + networkLab.textColor = Self.batteryDefaultTextColor + + // MQTT / 接口已有电量则展示,否则 unknown + let percent = Int(battery.int) + if percent > 0 { + applyBatteryDisplay(percent: min(percent, 100)) + } else { + applyBatteryUnknown() + } + } + + private func applyBatteryUnknown() { + batteryIcon.image = UIImage(named: "Home/member_battery_unknown") + batteryFillView.isHidden = true + batteryFillView.layoutChain.width(0) + batteryLab.text = "***" + batteryLab.textColor = Self.batteryDefaultTextColor + } + + private func applyBatteryDisplay(percent: Int?) { + guard let percent else { + applyBatteryUnknown() + return + } + let p = min(max(percent, 0), 100) + let isLow = p < 20 + let color = isLow ? Self.batteryLowColor : Self.batteryNormalColor + batteryIcon.image = UIImage(named: isLow ? "Home/member_battery_low" : "Home/member_battery") + batteryFillView.isHidden = false + batteryFillView.backgroundColor = color + batteryFillView.layoutChain.width(Self.batteryFillMaxWidth * CGFloat(p) / 100.0) + batteryLab.text = "\(p)%" + batteryLab.textColor = color + } + + // MARK: - Local device helpers + + private static func localPhoneModelName() -> String { + var systemInfo = utsname() + uname(&systemInfo) + let identifier = withUnsafePointer(to: &systemInfo.machine.0) { ptr in + String(cString: ptr) + } + return deviceModelMap[identifier] ?? "iPhone" + } + + private static func localBatteryPercent() -> Int? { + UIDevice.current.isBatteryMonitoringEnabled = true + let level = UIDevice.current.batteryLevel + guard level >= 0 else { return nil } + return Int((level * 100).rounded()) + } + + private static func localNetworkStatusText() -> String { + let path = NetworkStatusMonitor.shared.currentPath + if path.status != .satisfied { + return "无网络" + } + if path.usesInterfaceType(.wifi) { + return "Wi-Fi" + } + if path.usesInterfaceType(.cellular) { + return "蜂窝" + } + return "无网络" + } + + private static let deviceModelMap: [String: String] = [ + "iPhone10,1": "iPhone 8", + "iPhone10,4": "iPhone 8", + "iPhone10,2": "iPhone 8 Plus", + "iPhone10,5": "iPhone 8 Plus", + // iPhone X系列 + "iPhone10,3": "iPhone X", + "iPhone10,6": "iPhone X", + "iPhone11,2": "iPhone XS", + "iPhone11,4": "iPhone XS Max", + "iPhone11,6": "iPhone XS Max", + "iPhone11,8": "iPhone XR", + // iPhone11 + "iPhone12,1": "iPhone 11", + "iPhone12,3": "iPhone 11 Pro", + "iPhone12,5": "iPhone 11 Pro Max", + // iPhone12 + "iPhone13,1": "iPhone 12 mini", + "iPhone13,2": "iPhone 12", + "iPhone13,3": "iPhone 12 Pro", + "iPhone13,4": "iPhone 12 Pro Max", + // iPhone13 + "iPhone14,4": "iPhone 13 mini", + "iPhone14,5": "iPhone 13", + "iPhone14,2": "iPhone 13 Pro", + "iPhone14,3": "iPhone 13 Pro Max", + // iPhone14 + "iPhone14,7": "iPhone 14", + "iPhone14,8": "iPhone 14 Plus", + "iPhone15,2": "iPhone 14 Pro", + "iPhone15,3": "iPhone 14 Pro Max", + // iPhone15 + "iPhone15,4": "iPhone 15", + "iPhone15,5": "iPhone 15 Plus", + "iPhone16,1": "iPhone 15 Pro", + "iPhone16,2": "iPhone 15 Pro Max", + // iPhone16 + "iPhone17,1": "iPhone 16 Pro", + "iPhone17,2": "iPhone 16 Pro Max", + "iPhone17,3": "iPhone 16", + "iPhone17,4": "iPhone 16 Plus", + "iPhone17,5": "iPhone 16e", + // iPhone17 2025机型 + "iPhone18,1": "iPhone 17 Pro", + "iPhone18,2": "iPhone 17 Pro Max", + "iPhone18,3": "iPhone 17", + "iPhone18,4": "iPhone Air", + // Simulator + "i386": "Simulator", + "x86_64": "Simulator", + "arm64": "Simulator" + ] private func setupRx() { @@ -74,9 +261,30 @@ class GroupMemberView2: UIView { let view = UIView() view.backgroundColor = .clear view.clipsToBounds = true + + let icon = UIImageView(image: UIImage(named: "Home/member_logo")) + view.addSubview(icon) + icon.layoutChain + .top(kStatusBarHeight + 12) + .left(30) + + view.addSubview(inviteBtn) + inviteBtn.layoutChain + .centerY(icon) + .right(15) + .width(32) + .heightToWidth(1) + return view }() + lazy var inviteBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.setImage(UIImage(named: "Home/member_invite"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 10, right: 15) + return btn + }() + lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = .clear @@ -88,7 +296,12 @@ class GroupMemberView2: UIView { scrollContentView.addSubview(memberView) memberView.layoutChain - .edges() + .edges(excludingEdge: .bottom) + + scrollContentView.addSubview(todayTrackView) + todayTrackView.layoutChain + .topToBottomOfView(memberView, offset: 16) + .edges(excludingEdge: .top) return view }() @@ -135,7 +348,9 @@ class GroupMemberView2: UIView { .edgesHorzontal(15) .height(100) - memberCV.layoutChain.edges(all: 5) + memberCV.layoutChain + .edges(all: 5, excludingEdge: .bottom) + .bottom() view.addSubview(memberInfoView) memberInfoView.layoutChain @@ -162,13 +377,14 @@ class GroupMemberView2: UIView { btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium) btn.setImage(UIImage(named: "Common/arrow_right"), for: .normal) btn.semanticContentAttribute = .forceRightToLeft + btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 10, right: 15) return btn }() /// 成员列表 lazy var memberCV: UICollectionView = { let layout = UICollectionViewFlowLayout() - layout.itemSize = CGSize(width: 61, height: 90) + layout.itemSize = CGSize(width: 61, height: 95) layout.minimumLineSpacing = 15 layout.sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) layout.scrollDirection = .horizontal @@ -224,20 +440,47 @@ class GroupMemberView2: UIView { locationIcon.layoutChain .topToBottomOfView(memberNameLab, offset: 17) .left(20) - .width(10) - .height(10) + .width(16) + .heightToWidth(1) view.addSubview(locationLab) locationLab.layoutChain .centerY(locationIcon) .leftToRightOfView(locationIcon, offset: 7) + .compressionHorizontal(.defaultLow) view.addSubview(distanceView) distanceView.layoutChain .right(18) .centerY(locationLab) + .compressionHorizontal(.required) - locationLab.layoutChain.rightToLeftOfView(distanceView, offset: -30, relation: .greaterThanOrEqual) + locationLab.layoutChain.rightToLeftOfView(distanceView, offset: -30, relation: .lessThanOrEqual) + + view.addSubview(phoneModelView) + view.addSubview(batteryView) + view.addSubview(networkView) + view.addSubview(weatherView) + + phoneModelView.layoutChain + .topToBottomOfView(locationLab, offset: 20) + .left() + + batteryView.layoutChain + .topToView(phoneModelView) + .leftToRightOfView(phoneModelView) + .widthToView(phoneModelView) + + networkView.layoutChain + .topToView(phoneModelView) + .leftToRightOfView(batteryView) + .widthToView(phoneModelView) + + weatherView.layoutChain + .topToView(phoneModelView) + .leftToRightOfView(networkView) + .right() + .widthToView(phoneModelView) return view }() @@ -245,7 +488,7 @@ class GroupMemberView2: UIView { lazy var memberNameLab: UILabel = { let label = UILabel() label.text = " " - label.font = .systemFont(ofSize: 12, weight: .bold) + label.font = FontManager.youSheBiaoTiHei(12) label.textColor = UIColor(hexStr: "#353B4F") return label }() @@ -312,7 +555,7 @@ class GroupMemberView2: UIView { let label = UILabel() label.textColor = UIColor(hexStr: "#58EDFF") - label.font = .systemFont(ofSize: 10, weight: .bold) + label.font = FontManager.youSheBiaoTiHei(10) label.text = "距离" view.addSubview(label) @@ -332,15 +575,365 @@ class GroupMemberView2: UIView { lazy var distanceLab: UILabel = { let label = UILabel() label.textColor = .white + label.font = FontManager.youSheBiaoTiHei(10) + return label + }() + + /// 手机型号 + lazy var phoneModelView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + view.addSubview(phoneModelIcon) + phoneModelIcon.layoutChain + .top() + .centerX() + .width(24) + .height(24) + + view.addSubview(phoneModelLab) + phoneModelLab.layoutChain + .topToBottomOfView(phoneModelIcon, offset: 5) + .edgesHorzontal(5) + .bottom() + + return view + }() + + lazy var phoneModelIcon: UIImageView = { + let view = UIImageView(image: UIImage(named: "Home/member_phone_unknown")) + view.contentMode = .scaleAspectFit + return view + }() + + lazy var phoneModelLab: UILabel = { + let label = UILabel() + label.text = "***" + label.textColor = UIColor(hexStr: "#353B4F") + label.font = .systemFont(ofSize: 12, weight: .medium) + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + /// 电量 + lazy var batteryView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + view.addSubview(batteryFillView) + view.addSubview(batteryIcon) + batteryIcon.layoutChain + .top() + .centerX() + .width(24) + .height(24) + + // 填充层叠在轮廓下方,内边距避开描边与右侧电极 + batteryFillView.layoutChain + .centerY(batteryIcon) + .leftToView(batteryIcon, offset: 2) + .height(12) + .width(0) + + view.addSubview(batteryLab) + batteryLab.layoutChain + .topToBottomOfView(batteryIcon, offset: 5) + .centerX() + .bottom() + return view + }() + + lazy var batteryFillView: UIView = { + let view = UIView() + view.backgroundColor = Self.batteryNormalColor + view.isHidden = true + return view + }() + + lazy var batteryIcon: UIImageView = { + let view = UIImageView(image: UIImage(named: "Home/member_battery_unknown")) + view.contentMode = .scaleAspectFit + return view + }() + + lazy var batteryLab: UILabel = { + let label = UILabel() + label.text = "***" + label.textColor = UIColor(hexStr: "#353B4F") + label.font = .systemFont(ofSize: 12, weight: .medium) + return label + }() + + /// 手机网络 + lazy var networkView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + view.addSubview(networkIcon) + networkIcon.layoutChain + .top() + .centerX() + .width(24) + .height(24) + + view.addSubview(networkLab) + networkLab.layoutChain + .topToBottomOfView(networkIcon, offset: 5) + .centerX() + .bottom() + return view + }() + + lazy var networkIcon: UIImageView = { + let view = UIImageView(image: UIImage(named: "Home/member_network_unknown")) + view.contentMode = .scaleAspectFit + return view + }() + + lazy var networkLab: UILabel = { + let label = UILabel() + label.text = "***" + label.textColor = UIColor(hexStr: "#353B4F") + label.font = .systemFont(ofSize: 12, weight: .medium) + return label + }() + + /// 天气 + lazy var weatherView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + let icon = UIImageView(image: UIImage(named: "Home/member_weather_unknown")) + view.addSubview(icon) + icon.layoutChain + .top() + .centerX() + .width(24) + .height(24) + + view.addSubview(weatherLab) + weatherLab.layoutChain + .topToBottomOfView(icon, offset: 5) + .centerX() + .bottom() + return view + }() + + lazy var weatherLab: UILabel = { + let label = UILabel() + label.text = "***" + label.textColor = UIColor(hexStr: "#353B4F") + label.font = .systemFont(ofSize: 12, weight: .medium) + return label + }() + + // MARK: - 今日轨迹 + lazy var todayTrackView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + let icon = UIImageView() + icon.image = UIImage(named: "Home/member_today_track") + view.addSubview(icon) + icon.layoutChain + .top() + .left(15) + .width(18) + .heightToWidth(1) + + let titleLab = UILabel() + titleLab.text = "今日轨迹" + titleLab.textColor = UIColor(hexStr: "#293445") + titleLab.font = .systemFont(ofSize: 16, weight: .bold) + view.addSubview(titleLab) + titleLab.layoutChain + .leftToRightOfView(icon, offset: 6) + .centerY(icon) + + let infoView = UIView() + infoView.backgroundColor = .white + infoView.cornerRadius = 16 + view.addSubview(infoView) + infoView.layoutChain + .topToBottomOfView(icon, offset: 12) + .edgesHorzontal(15) + .bottom() + + let mapIcon = UIImageView(image: UIImage(named: "Home/member_map")) + infoView.addSubview(mapIcon) + mapIcon.layoutChain + .left(12) + .edgesVertical(15) + .width(80) + .heightToWidth(1) + + let locationIcon = UIImageView(image: UIImage(named: "Home/member_location")) + infoView.addSubview(locationIcon) + locationIcon.layoutChain + .topToView(mapIcon, offset: 11) + .leftToRightOfView(mapIcon, offset: 14) + .width(16) + .heightToWidth(1) + + let stayTxtLab = UILabel() + stayTxtLab.text = "今天停留了" + stayTxtLab.textColor = UIColor(hexStr: "#293445") + stayTxtLab.font = .systemFont(ofSize: 16, weight: .medium) + view.addSubview(stayTxtLab) + stayTxtLab.layoutChain + .leftToRightOfView(locationIcon, offset: 6) + .centerY(locationIcon) + + let stayNumView = UIView() + stayNumView.backgroundColor = UIColor(hexStr: "#293445") + stayNumView.cornerRadius = 6 + infoView.addSubview(stayNumView) + stayNumView.layoutChain + .leftToRightOfView(stayTxtLab) + .centerY(stayTxtLab) + + stayNumView.addSubview(stayNumLab) + stayNumLab.layoutChain + .edgesHorzontal(5) + .edgesVertical() + + let stayTxtLab1 = UILabel() + stayTxtLab1.text = "个地方" + stayTxtLab1.textColor = UIColor(hexStr: "#293445") + stayTxtLab1.font = .systemFont(ofSize: 16, weight: .medium) + view.addSubview(stayTxtLab1) + stayTxtLab1.layoutChain + .leftToRightOfView(stayNumView, offset: 0) + .centerY(stayTxtLab) + + view.addSubview(stayLocationView) + stayLocationView.layoutChain + .topToBottomOfView(locationIcon, offset: 12) + .leftToRightOfView(mapIcon, offset: 14) + .right() + .bottomToView(mapIcon) + + view.addSubview(stayNodataView) + stayNodataView.layoutChain + .topToBottomOfView(stayTxtLab, offset: 14) + .leftToView(stayTxtLab) + + return view + }() + + lazy var stayNumLab: UILabel = { + let label = UILabel() + label.text = "0" + label.textColor = UIColor(hexStr: "#58EDFF") + label.font = FontManager.youSheBiaoTiHei(14) + return label + }() + + lazy var stayLocationView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.isHidden = true + + let nowDotView = UIView() + nowDotView.backgroundColor = UIColor(hexStr: "#3DC0FF") + nowDotView.cornerRadius = 6 + view.addSubview(nowDotView) + nowDotView.layoutChain + .top() + .left() + .width(12) + .heightToWidth(1) + + let dotView = UIView() + dotView.backgroundColor = .white + dotView.cornerRadius = 3 + nowDotView.addSubview(dotView) + dotView.layoutChain + .centerX() + .centerY() + .width(6) + .heightToWidth(1) + + let dashLine = DashLineView() + dashLine.backgroundColor = .clear + view.addSubview(dashLine) + dashLine.layoutChain + .topToBottomOfView(nowDotView) + .centerX(nowDotView) + .width(1) + .bottom() + + view.addSubview(stayLocationLab) + stayLocationLab.layoutChain + .leftToRightOfView(nowDotView, offset: 3) + .centerY(nowDotView) + .right(15) + + let stayStatusView = UIView() + stayStatusView.backgroundColor = UIColor(hexStr: "#F2F2F2") + stayStatusView.cornerRadius = 6 + view.addSubview(stayStatusView) + stayStatusView.layoutChain + .topToBottomOfView(stayLocationLab, offset: 9) + .leftToView(stayLocationLab) + + let clockIcon = UIImageView(image: UIImage(named: "Home/member_clock")) + stayStatusView.addSubview(clockIcon) + clockIcon.layoutChain + .edges(all: 4, excludingEdge: .right) + .width(12) + .heightToWidth(1) + + stayStatusView.addSubview(stayStatusLab) + stayStatusLab.layoutChain + .leftToRightOfView(clockIcon, offset: 2) + .right(7) + .centerY() + + return view + }() + + lazy var stayLocationLab: UILabel = { + let label = UILabel() + label.text = "当前 xxx 附近" + label.textColor = UIColor(hexStr: "#293445") label.font = .systemFont(ofSize: 10, weight: .bold) return label }() + lazy var stayStatusLab: UILabel = { + let label = UILabel() + label.text = "停留中..." // 停留多少时间 + label.textColor = UIColor(hexStr: "#293445") + label.font = .systemFont(ofSize: 8, weight: .bold) + return label + }() + + lazy var stayNodataView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#F2F2F2") + view.cornerRadius = 6 + view.isHidden = false + + let label = UILabel() + label.text = "暂时没有停留记录" + label.textColor = UIColor(hexStr: "#293445") + label.font = .systemFont(ofSize: 12, weight: .medium) + view.addSubview(label) + label.layoutChain + .edgesHorzontal(6) + .edgesVertical(9) + + return view + }() + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#FAFAFA") cornerRadius = 30 layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + _ = NetworkStatusMonitor.shared setupUI() setupRx() } @@ -350,3 +943,19 @@ class GroupMemberView2: UIView { } } + +/// 轻量网络状态监听,供面板读取当前路径 +private final class NetworkStatusMonitor { + static let shared = NetworkStatusMonitor() + + private let monitor = NWPathMonitor() + private(set) var currentPath: NWPath + + private init() { + currentPath = monitor.currentPath + monitor.pathUpdateHandler = { [weak self] path in + self?.currentPath = path + } + monitor.start(queue: DispatchQueue(label: "com.quicklocation.networkstatus")) + } +} diff --git a/QuickLocation/Section/Home/HomeView2.swift b/QuickLocation/Section/Home/HomeView2.swift index e95351bc..631d66dd 100644 --- a/QuickLocation/Section/Home/HomeView2.swift +++ b/QuickLocation/Section/Home/HomeView2.swift @@ -91,8 +91,8 @@ class HomeView2: UIView { locationView.layoutChain .bottomToTopOfView(inviteBtn, offset: -8) .left(15) - .width(40) - .height(40) + .width(32) + .heightToWidth(1) groupMemberTopConstraint = groupMemberView.jh_constraint( .top, toAttribute: .top, otherView: self, relation: .equal @@ -183,7 +183,13 @@ class HomeView2: UIView { view.cornerRadius = 8 view.isHidden = true - let icon = UIImageView() + let icon = UIImageView(image: UIImage(named: "Home/warning")) + view.addSubview(icon) + icon.layoutChain + .left(8) + .width(14) + .heightToWidth(1) + .centerY() let label = UILabel() label.text = "有权限未开启,数据可能会异常" @@ -191,7 +197,8 @@ class HomeView2: UIView { label.font = .systemFont(ofSize: 12, weight: .medium) view.addSubview(label) label.layoutChain - .edgesHorzontal(8) + .leftToRightOfView(icon, offset: 6) + .right(8) .edgesVertical(10) return view @@ -200,8 +207,8 @@ class HomeView2: UIView { // MARK: - 定位按钮 lazy var locationView: UIView = { let view = UIView() - view.backgroundColor = .black.withAlphaComponent(0.4) - view.cornerRadius = 20 + view.backgroundColor = .white + view.cornerRadius = 10 view.addSubview(locationIconView) locationIconView.layoutChain.centerX().centerY() diff --git a/QuickLocation/Section/Home/HomeViewController.swift b/QuickLocation/Section/Home/HomeViewController.swift index f24781df..a846c3af 100644 --- a/QuickLocation/Section/Home/HomeViewController.swift +++ b/QuickLocation/Section/Home/HomeViewController.swift @@ -49,6 +49,10 @@ class HomeViewController: BaseViewController { /// 底部成员面板是否显示 private var isMemberPanelShown = false private var isSOSPlaying = false + /// SOS 闪烁截止时间(userId → Date) + private var sosBlinkUntil: [String: Date] = [:] + /// SOS 闪烁定时任务(userId → WorkItem) + private var sosBlinkWorkItems: [String: DispatchWorkItem] = [:] /// 记录每个成员最后一次收到位置的时间(MQTT userId → Date) private var lastUpdateTimes: [String: Date] = [:] /// 离线检测定时器 @@ -166,6 +170,15 @@ class HomeViewController: BaseViewController { // AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key]) // }.disposed(by: disposeBag) + // 切换圈子 + rootView.groupMemberView.changeGroupBtn.rx.tap.subscribe(onNext: { [weak self] in + guard let self = self, let groupModel = self.viewModel.groupModel else { return } + GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in + guard let self = self, let key = groupKey else { return } + self.requestOperateGroup(groupKey: key) + } + }).disposed(by: disposeBag) + // 顶部圈子 // rootView.groupView.rx.tapGesture.subscribe { _ in // guard let groupModel = self.viewModel.groupModel else { return } @@ -188,10 +201,14 @@ class HomeViewController: BaseViewController { AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.viewModel.groupInfo]) }).disposed(by: disposeBag) + rootView.groupMemberView.inviteBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.viewModel.groupInfo]) + }).disposed(by: disposeBag) + // 地图回到自己 rootView.locationView.rx.tapGesture.subscribe { _ in if let ann = self.currentUserAnnotation { - self.rootView.mapView.setCenter(ann.coordinate, animated: true) + self.selectMember(userId: ann.member.id, centerMap: true, dismissPanel: false) } }.disposed(by: disposeBag) @@ -325,9 +342,11 @@ class HomeViewController: BaseViewController { lazy private var dataSource: RxCollectionViewSectionedReloadDataSource = { RxCollectionViewSectionedReloadDataSource { [weak self] _, collectionView, indexPath, model in let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath) + let blinking = self?.isSOSBlinking(userId: model.user_id) ?? false cell.configure(model: model, isCurrentUser: self?.viewModel.isCurrentUser(id: model.user_id) ?? false, - isSelected: model.user_id == self?.selectedMemberId) + isSelected: model.user_id == self?.selectedMemberId, + isSOSBlinking: blinking) return cell } }() @@ -416,17 +435,19 @@ class HomeViewController: BaseViewController { @objc private func handleMapTap(_ tap: UITapGestureRecognizer) { let point = tap.location(in: rootView.mapView) - // 用 mapView 的坐标转屏幕坐标判断命中 + // 色点对准坐标,气泡在上方,命中区需向上覆盖整颗标注 + let hitWidth: CGFloat = 70 + let hitHeight: CGFloat = 110 for ann in rootView.mapView.annotations?.compactMap({ $0 as? MemberAnnotation }) ?? [] { - guard !ann.member.isCurrentUser else { continue } let pt = rootView.mapView.convert(ann.coordinate, toPointTo: rootView.mapView) - // 30pt 命中半径 - let hitRect = CGRect(x: pt.x - 30, y: pt.y - 30, width: 60, height: 60) + let hitRect = CGRect( + x: pt.x - hitWidth / 2, + y: pt.y - hitHeight + 12, + width: hitWidth, + height: hitHeight + ) if hitRect.contains(point) { - self.isMemberPanelShown = true - (self.tabBarController as? MainTabBarController)?.setTabBarHidden(true) -// rootView.showMemberPanel(member: ann.member) - viewModel.targetUid = ann.member.id + selectMember(userId: ann.member.id, centerMap: true, dismissPanel: false) break } } @@ -532,18 +553,23 @@ class HomeViewController: BaseViewController { #endif } + /// 若更新的是当前选中成员,刷新信息卡(位置 / 在线 / 距离等) + private func refreshSelectedMemberInfoIfNeeded(userId: String) { + guard userId == selectedMemberId, + let model = viewModel.memberList.first(where: { $0.user_id == userId }) else { return } + rootView.groupMemberView.setupMemberInfo( + model, + isOwner: viewModel.isGroupOwn(id: model.user_id) + ) + } + /// 选中成员:同步列表高亮 / 信息卡,并可选居中地图、收起面板 private func selectMember(userId: String, centerMap: Bool = true, dismissPanel: Bool = false) { let listUserId = (userId == "current") ? AppContextManager.shared.userId : userId guard !listUserId.isEmpty else { return } selectedMemberId = listUserId - if let model = viewModel.memberList.first(where: { $0.user_id == listUserId }) { - rootView.groupMemberView.setupMemberInfo( - model, - isOwner: viewModel.isGroupOwn(id: model.user_id) - ) - } + refreshSelectedMemberInfoIfNeeded(userId: listUserId) memberCV.reloadData() if let idx = viewModel.memberList.firstIndex(where: { $0.user_id == listUserId }) { @@ -672,11 +698,13 @@ extension HomeViewController { } else if !isOnline { self.removeAnnotation(userId: userId) } + self.refreshSelectedMemberInfoIfNeeded(userId: userId) } case "disconnect": // 离线 self.removeAnnotation(userId: userId) viewModel.setMemberOffline(userId: userId) updateOnlineCount() + refreshSelectedMemberInfoIfNeeded(userId: userId) case "emote": // 快捷消息、表情 guard let userId = msg.data?.user_id, userId == AppContextManager.shared.userId, // 只接收发给我的 @@ -744,6 +772,8 @@ extension HomeViewController { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.viewModel.updateMemberStatus(userId: userId, status: 1) + self.startSOSBlink(userId: userId) +// self.selectMember(userId: userId, centerMap: true, dismissPanel: false) guard !self.isSOSPlaying else { return } self.isSOSPlaying = true @@ -753,13 +783,11 @@ extension HomeViewController { // self.rootView.sosPopView.stop() // self.rootView.sosPopView.isHidden = true // } -// DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak self] in -// self?.sosPlayer?.stop() -// self?.sosPlayer = nil -// self?.rootView.sosPopView.stop() -// self?.rootView.sosPopView.isHidden = true -// self?.isSOSPlaying = false -// } + DispatchQueue.main.asyncAfter(deadline: .now() + 30) { [weak self] in + self?.sosPlayer?.stop() + self?.sosPlayer = nil + self?.isSOSPlaying = false + } } case "join": // 圈子有成员加入 guard let gk = msg.data?.group_key, @@ -895,6 +923,12 @@ extension HomeViewController: MAMapViewDelegate { return annotationView } + func mapView(_ mapView: MAMapView!, didSelect view: MAAnnotationView!) { + guard let ann = view.annotation as? MemberAnnotation else { return } + mapView.deselectAnnotation(ann, animated: false) + selectMember(userId: ann.member.id, centerMap: true, dismissPanel: false) + } + func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) { guard updatingLocation, let location = userLocation.location else { return } @@ -944,6 +978,27 @@ extension HomeViewController { set { objc_setAssociatedObject(self, &HomeViewController.sosPlayerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } + private func isSOSBlinking(userId: String) -> Bool { + guard let until = sosBlinkUntil[userId] else { return false } + return until > Date() + } + + /// 启动 / 重置该成员 15 秒 SOS 头像闪烁 + private func startSOSBlink(userId: String) { + sosBlinkWorkItems[userId]?.cancel() + sosBlinkUntil[userId] = Date().addingTimeInterval(15) + memberCV.reloadData() + + let work = DispatchWorkItem { [weak self] in + guard let self = self else { return } + self.sosBlinkUntil.removeValue(forKey: userId) + self.sosBlinkWorkItems.removeValue(forKey: userId) + self.memberCV.reloadData() + } + sosBlinkWorkItems[userId] = work + DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: work) + } + private func playSOSAlarm() { // 停止上一次报警 sosPlayer?.stop() diff --git a/QuickLocation/Section/Login/OneTapLoginView.swift b/QuickLocation/Section/Login/OneTapLoginView.swift index 2b9f4923..d90c8744 100644 --- a/QuickLocation/Section/Login/OneTapLoginView.swift +++ b/QuickLocation/Section/Login/OneTapLoginView.swift @@ -166,7 +166,7 @@ class OneTapLoginView: UIView { lazy var welcomeLabel: UILabel = { let label = UILabel() // label.text = "欢迎登录" - label.font = UIFont(name: "DOUYU Font", size: 24) ?? .systemFont(ofSize: 24, weight: .medium) + label.font = .systemFont(ofSize: 24, weight: .medium) label.textColor = UIColor(hexStr: "#030303") label.textAlignment = .center return label diff --git a/QuickLocation/Section/Map/CircleMember.swift b/QuickLocation/Section/Map/CircleMember.swift index 6032a319..a72671c0 100644 --- a/QuickLocation/Section/Map/CircleMember.swift +++ b/QuickLocation/Section/Map/CircleMember.swift @@ -38,7 +38,7 @@ extension CircleMember { } /// 解析 last_position:格式 "lat:lng:address" - private static func parsePosition(_ position: String) -> (CLLocationCoordinate2D, String) { + static func parsePosition(_ position: String) -> (CLLocationCoordinate2D, String) { guard !position.isEmpty else { return (kCLLocationCoordinate2DInvalid, "") } let parts = position.components(separatedBy: ":") guard parts.count >= 2, diff --git a/QuickLocation/Section/Map/MemberAnnotationView.swift b/QuickLocation/Section/Map/MemberAnnotationView.swift index c86716be..fbfdf42b 100644 --- a/QuickLocation/Section/Map/MemberAnnotationView.swift +++ b/QuickLocation/Section/Map/MemberAnnotationView.swift @@ -16,8 +16,8 @@ final class MemberAnnotationView: MAAnnotationView { private static let bubbleSize: CGFloat = 56 private static let bubbleCorner: CGFloat = 14 private static let borderWidth: CGFloat = 4 - private static let pointerWidth: CGFloat = 12 - private static let pointerHeight: CGFloat = 7 + private static let pointerWidth: CGFloat = 14 + private static let pointerHeight: CGFloat = 6 private static let gapBubbleToDot: CGFloat = 4 private static let gapNameToBubble: CGFloat = 4 private static let nameTagHeight: CGFloat = 20 @@ -87,10 +87,10 @@ final class MemberAnnotationView: MAAnnotationView { return iv }() - private let pointerLayer: CAShapeLayer = { - let layer = CAShapeLayer() - layer.fillColor = UIColor.white.cgColor - return layer + private let pointerView: UIImageView = { + let iv = UIImageView(image: UIImage(named: "Map/arrow")) + iv.contentMode = .scaleAspectFit + return iv }() private let dotOuterView: UIView = { @@ -130,13 +130,15 @@ final class MemberAnnotationView: MAAnnotationView { private func setupUI() { canShowCallout = false isEnabled = true + isUserInteractionEnabled = true addSubview(containerView) + containerView.isUserInteractionEnabled = true containerView.addSubview(nameTagView) nameTagView.addSubview(nameLabel) - containerView.layer.addSublayer(pointerLayer) containerView.addSubview(bubbleView) bubbleView.addSubview(avatarImageView) + containerView.addSubview(pointerView) containerView.addSubview(dotOuterView) dotOuterView.addSubview(dotInnerView) dotInnerView.addSubview(dotCenterView) @@ -145,6 +147,7 @@ final class MemberAnnotationView: MAAnnotationView { // MARK: - Configure func configure(with member: CircleMember) { isEnabled = true + isUserInteractionEnabled = true canShowCallout = false if let img = UIImage(named: "UserIcon/\(member.avatar)") { @@ -226,17 +229,16 @@ final class MemberAnnotationView: MAAnnotationView { width: inner, height: inner ) - // 白色三角:尖朝下,顶边贴气泡底边 + // 箭头:贴气泡底边居中 let pointerTop = bubbleY + bubble - 1 - let pointerPath = UIBezierPath() - let leftX = (totalWidth - Self.pointerWidth) / 2 - pointerPath.move(to: CGPoint(x: leftX, y: pointerTop)) - pointerPath.addLine(to: CGPoint(x: leftX + Self.pointerWidth, y: pointerTop)) - pointerPath.addLine(to: CGPoint(x: totalWidth / 2, y: pointerTop + pointerH)) - pointerPath.close() - pointerLayer.path = pointerPath.cgPath + pointerView.frame = CGRect( + x: (totalWidth - Self.pointerWidth) / 2, + y: pointerTop, + width: Self.pointerWidth, + height: Self.pointerHeight + ) - let dotY = bubbleY + bubble + pointerH + gap + let dotY = bubbleY + bubble + Self.pointerHeight + gap dotOuterView.frame = CGRect( x: (totalWidth - dot) / 2, y: dotY, width: dot, height: dot diff --git a/QuickLocation/Section/PopupWindow/PopupWindow.swift b/QuickLocation/Section/PopupWindow/PopupWindow.swift index dc2722d4..09aad3e0 100644 --- a/QuickLocation/Section/PopupWindow/PopupWindow.swift +++ b/QuickLocation/Section/PopupWindow/PopupWindow.swift @@ -520,7 +520,7 @@ class PopupWindow: UIView { .strokeWidth: -5, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor.white, - .font: UIFont(name: "zihunbiantaoti", size: 20) ?? .systemFont(ofSize: 20) + .font: FontManager.ziHunBianHei(20) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count)) titleLab.attributedText = attr @@ -537,7 +537,7 @@ class PopupWindow: UIView { .strokeWidth: -5, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor.white, - .font: UIFont(name: "zihunbiantaoti", size: 22) ?? .systemFont(ofSize: 22) + .font: FontManager.ziHunBianHei(22) ] countAttr.setAttributes(countStrokeAttrs, range: NSRange(location: 0, length: countText.count)) countTitleLab.attributedText = countAttr @@ -603,7 +603,7 @@ class PopupWindow: UIView { .strokeWidth: -5, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor(hexStr: "#7CE7FC"), - .font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34) + .font: FontManager.ziHunBianHei(34) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count)) label.attributedText = attr @@ -669,7 +669,7 @@ class PopupWindow: UIView { .strokeWidth: -3, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor(hexStr: "#7CE7FC"), - .font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34) + .font: FontManager.ziHunBianHei(34) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count)) self?.unlockVip2CountLab.attributedText = attr @@ -688,7 +688,7 @@ class PopupWindow: UIView { .strokeWidth: -3, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor(hexStr: "#7CE7FC"), - .font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34) + .font: FontManager.ziHunBianHei(34) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: 2)) unlockVip2CountLab.attributedText = attr