- 选择圈子

- 邀请加入
- 圈子/行程
- 底部tabbar图标更换
This commit is contained in:
linshujie 2026-08-05 18:35:18 +08:00
parent 69e38f19fe
commit 035137a776
107 changed files with 3554 additions and 695 deletions

View File

@ -96,6 +96,7 @@
305A76DE2FCA8C7000227D26 /* ObservableType+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */; };
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */; };
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76232FCA8C7000227D26 /* GroupView.swift */; };
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; };
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76242FCA8C7000227D26 /* GroupViewController.swift */; };
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; };
305A76E32FCA8C7000227D26 /* GroupMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76272FCA8C7000227D26 /* GroupMemberView.swift */; };
@ -151,7 +152,6 @@
305A771C2FCA8C7000227D26 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A767E2FCA8C7000227D26 /* AppDelegate.swift */; };
305A771D2FCA8C7000227D26 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76862FCA8C7000227D26 /* ViewController.swift */; };
305A771E2FCA8C7000227D26 /* adhoc.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 305A74D12FCA8C7000227D26 /* adhoc.xcconfig */; };
305A771F2FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */; };
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */; };
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 305A767F2FCA8C7000227D26 /* Assets.xcassets */; };
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 305A76822FCA8C7000227D26 /* LaunchScreen.storyboard */; };
@ -296,6 +296,9 @@
557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; };
55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; };
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179230218CE100784774 /* GroupMemberView2.swift */; };
55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */; };
55B217A13022F0A000784702 /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217A03022F0A000784701 /* FontManager.swift */; };
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; };
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */; };
@ -386,7 +389,6 @@
305A752F2FCA8C7000227D26 /* SystemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemResponse.swift; sourceTree = "<group>"; };
305A75312FCA8C7000227D26 /* ImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlugin.swift; sourceTree = "<group>"; };
305A75322FCA8C7000227D26 /* NotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEmpty.swift; sourceTree = "<group>"; };
305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "D-DIN-PRO-500-Medium.otf"; sourceTree = "<group>"; };
305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "zihunbiantaoti-r.ttf"; sourceTree = "<group>"; };
305A761A2FCA8C7000227D26 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = "<group>"; };
305A761B2FCA8C7000227D26 /* Action+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+Internal.swift"; sourceTree = "<group>"; };
@ -397,6 +399,7 @@
305A76202FCA8C7000227D26 /* ObservableType+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ObservableType+ObjectMapper.swift"; sourceTree = "<group>"; };
305A76212FCA8C7000227D26 /* Single+ObjectMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Single+ObjectMapper.swift"; sourceTree = "<group>"; };
305A76232FCA8C7000227D26 /* GroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupView.swift; sourceTree = "<group>"; };
55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; };
305A76242FCA8C7000227D26 /* GroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewController.swift; sourceTree = "<group>"; };
305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = "<group>"; };
305A76272FCA8C7000227D26 /* GroupMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView.swift; sourceTree = "<group>"; };
@ -605,6 +608,9 @@
557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = "<group>"; };
55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = "<group>"; };
55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = "<group>"; };
55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "荆南波波黑-Bold.ttf"; sourceTree = "<group>"; };
55B217A03022F0A000784701 /* FontManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontManager.swift; sourceTree = "<group>"; };
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
@ -784,6 +790,7 @@
305A75062FCA8C7000227D26 /* Core */ = {
isa = PBXGroup;
children = (
55B217A23022F0A000784703 /* Font */,
305A75042FCA8C7000227D26 /* Extension */,
305A75052FCA8C7000227D26 /* Wrapper.swift */,
);
@ -950,7 +957,8 @@
305A75392FCA8C7000227D26 /* Font */ = {
isa = PBXGroup;
children = (
305A75342FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf */,
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */,
55B217943022DD6B00784774 /* 荆南波波黑-Bold.ttf */,
305A75382FCA8C7000227D26 /* zihunbiantaoti-r.ttf */,
);
path = Font;
@ -989,6 +997,7 @@
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
305A76232FCA8C7000227D26 /* GroupView.swift */,
55B217B03022F0A000784710 /* GroupItineraryView.swift */,
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
307073E42FD18A20004C37CC /* GroupChat */,
@ -1666,6 +1675,14 @@
name = Frameworks;
sourceTree = "<group>";
};
55B217A23022F0A000784703 /* Font */ = {
isa = PBXGroup;
children = (
55B217A03022F0A000784701 /* FontManager.swift */,
);
path = Font;
sourceTree = "<group>";
};
B07D45692FCECE07570D9B73 /* Pods */ = {
isa = PBXGroup;
children = (
@ -1744,11 +1761,12 @@
buildActionMask = 2147483647;
files = (
305A771E2FCA8C7000227D26 /* adhoc.xcconfig in Resources */,
305A771F2FCA8C7000227D26 /* D-DIN-PRO-500-Medium.otf in Resources */,
305A77232FCA8C7000227D26 /* zihunbiantaoti-r.ttf in Resources */,
305A77F92FCA8C7000227D26 /* Assets.xcassets in Resources */,
A1B2C3D42FDABC8C009215C1 /* PrivacyInfo.xcprivacy in Resources */,
305A77FB2FCA8C7000227D26 /* LaunchScreen.storyboard in Resources */,
55B217953022DD6B00784774 /* 荆南波波黑-Bold.ttf in Resources */,
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */,
305A77FC2FCA8C7000227D26 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1911,6 +1929,7 @@
305A76AE2FCA8C7000227D26 /* UIButton+Extension.swift in Sources */,
305A76AF2FCA8C7000227D26 /* UIColor+Extension.swift in Sources */,
305A76B02FCA8C7000227D26 /* UIFont+Extension.swift in Sources */,
55B217A13022F0A000784702 /* FontManager.swift in Sources */,
305A76B12FCA8C7000227D26 /* UIImage+Extension.swift in Sources */,
305A76B22FCA8C7000227D26 /* UIImage+Resource.swift in Sources */,
305A76B32FCA8C7000227D26 /* UILabel+Extension.swift in Sources */,
@ -1999,6 +2018,7 @@
30D74AAB2FE8C7700050EB2C /* GPSSignalHelper.swift in Sources */,
30A87A6B2FEF5B950095E7C6 /* SearchLocationView.swift in Sources */,
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */,
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */,
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */,

View File

@ -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()

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "组 47778@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "组 47778@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "组 47776@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "组 47776@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "蒙版组 13863@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "蒙版组 13863@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "蒙版组 13862@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "蒙版组 13862@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "蒙版组 13862@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "蒙版组 13862@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "today_track@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "today_track@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "蒙版组 13878@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "蒙版组 13878@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "蒙版组 13863@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "蒙版组 13863@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -33,20 +33,37 @@ public extension UIFont {
}
///
/// - Parameter name:
/// - Parameter name: `xxx.ttf`
/// - Returns: Full Name `UIFont(name:)`
public static func loadFontName(_ fileName: String) -> String? {
//
guard let fontURL = Bundle.main.url(forResource: fileName, withExtension: nil),
let nsName = fileName as NSString
let resource = nsName.deletingPathExtension
let ext = nsName.pathExtension
let fontURL: URL?
if ext.isEmpty {
fontURL = Bundle.main.url(forResource: fileName, withExtension: nil)
} else {
fontURL = Bundle.main.url(forResource: resource, withExtension: ext)
?? Bundle.main.url(forResource: fileName, withExtension: nil)
}
guard let fontURL,
let data = try? Data(contentsOf: fontURL),
let provider = CGDataProvider(data: data as CFData),
let font = CGFont(provider) else { return nil }
//
CTFontManagerRegisterGraphicsFont(font, nil)
return String(font.fullName ?? "")
}
}
var error: Unmanaged<CFError>?
CTFontManagerRegisterGraphicsFont(font, &error)
// Full Name PostScript Name
if let full = font.fullName as String?, !full.isEmpty {
return full
}
if let ps = font.postScriptName as String?, !ps.isEmpty, !ps.hasPrefix("-") {
return ps
}
return nil
}
}
// MARK: - Montserrat
//public extension UIFont {
//

View File

@ -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 BoldPostScript -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
}
}

View File

@ -66,7 +66,8 @@
</dict>
<key>UIAppFonts</key>
<array>
<string>douyu.otf</string>
<string>荆南波波黑-Bold.ttf</string>
<string>优设标题黑_猫啃网.ttf</string>
<string>zihunbiantaoti-r.ttf</string>
</array>
<key>UIBackgroundModes</key>

View File

@ -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"),

View File

@ -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

View File

@ -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)")
}
}

Binary file not shown.

View File

@ -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)
}
}

View File

@ -650,12 +650,16 @@ class DateCell: UICollectionViewCell {
// MARK: - GroupMemberListCell
class GroupMemberListCell: UICollectionViewCell {
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) {
private static let sosBlinkKey = "sosBlink"
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool, isSOSBlinking: Bool = false) {
avaterImgView.image = model.userIcon
vipIcon.image = model.vipIcon
// vipIcon.image = model.vipIcon
vipBgImgView.isHidden = model.level == 1
nameLab.text = model.nick_name
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
selectedBgView.isHidden = !isSelected
updateSOSBlink(isSOSBlinking)
//
// if AppContextManager.shared.vip > 1, model.is_online {
// batteryInfoView.isHidden = model.battery.int == 0
@ -666,10 +670,36 @@ class GroupMemberListCell: UICollectionViewCell {
// }
}
private func updateSOSBlink(_ blinking: Bool) {
sosImgView.layer.removeAnimation(forKey: Self.sosBlinkKey)
if blinking {
sosImgView.isHidden = false
sosImgView.alpha = 1
let anim = CABasicAnimation(keyPath: "opacity")
anim.fromValue = 1.0
anim.toValue = 0.2
anim.duration = 0.6
anim.autoreverses = true
anim.repeatCount = .infinity
anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
sosImgView.layer.add(anim, forKey: Self.sosBlinkKey)
} else {
sosImgView.isHidden = true
sosImgView.alpha = 1
}
}
override func prepareForReuse() {
super.prepareForReuse()
updateSOSBlink(false)
}
private func setupSubviews() {
contentView.addSubview(selectedBgView)
contentView.addSubview(avaterImgView)
contentView.addSubview(vipIcon)
contentView.addSubview(sosImgView)
contentView.addSubview(vipBgImgView)
// contentView.addSubview(vipIcon)
contentView.addSubview(batteryInfoView)
batteryInfoView.addSubview(cornerView)
cornerView.addSubview(batteryView)
@ -688,6 +718,16 @@ class GroupMemberListCell: UICollectionViewCell {
.edgesHorzontal(5.5)
.heightToWidth(1)
sosImgView.layoutChain
.topToView(avaterImgView)
.leftToView(avaterImgView)
.rightToView(avaterImgView)
.bottomToView(avaterImgView)
vipBgImgView.layoutChain
.top(7)
.centerX()
batteryInfoView.layoutChain
.leftToView(avaterImgView)
.rightToView(avaterImgView)
@ -712,14 +752,14 @@ class GroupMemberListCell: UICollectionViewCell {
.right(5)
.centerY()
vipIcon.layoutChain
.topToView(avaterImgView, offset: -8)
.leftToView(avaterImgView, offset: -6)
.width(25)
.height(21)
// vipIcon.layoutChain
// .topToView(avaterImgView, offset: -8)
// .leftToView(avaterImgView, offset: -6)
// .width(25)
// .height(21)
nameLab.layoutChain
.topToBottomOfView(batteryInfoView, offset: 4)
.topToBottomOfView(batteryInfoView, offset: 8)
.edgesHorzontal()
@ -732,6 +772,13 @@ class GroupMemberListCell: UICollectionViewCell {
return view
}()
lazy var vipBgImgView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "GroupMemberList/vip")
view.isHidden = true
return view
}()
lazy var avaterImgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .lightGray
@ -740,6 +787,13 @@ class GroupMemberListCell: UICollectionViewCell {
return view
}()
lazy var sosImgView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "GroupMemberList/sos")
view.isHidden = true
return view
}()
lazy var batteryInfoView: UIView = {
let view = UIView()
view.backgroundColor = .clear

View File

@ -24,133 +24,228 @@ class GroupView: UIView {
var disposeBag = DisposeBag()
/// 0 = 1 =
private(set) var currentTopTab: Int = 0
///
private var isSticky = false
/// tableview
private var isSubCanScroll = false
var stickThreshold: CGFloat {
return segmentView.dl.y
// segment listContainer listContainer
return listContainer.dl.y
}
// MARK: - Setup
private func setupUI() {
addSubview(navBgView)
addSubview(navView)
navView.addRightButton(scanBtn)
addSubview(topBar)
topBar.addSubview(circleTabLabel)
topBar.addSubview(itineraryTabLabel)
topBar.addSubview(messageBtn)
addSubview(mainScrollView)
addSubview(circlePage)
circlePage.addSubview(mainScrollView)
mainScrollView.addSubview(contentView)
contentView.addSubview(cycleScrollView)
contentView.addSubview(actionButtonsView)
actionButtonsView.addSubview(createGroupBtn)
actionButtonsView.addSubview(joinGroupBtn)
contentView.addSubview(listContainer)
listContainer.addSubview(segmentView)
segmentView.addSubview(joinedTabLabel)
segmentView.addSubview(createdTabLabel)
segmentView.addSubview(tabIndicator)
listContainer.addSubview(segmentScrollView)
segmentScrollView.addSubview(segmentContentView)
// (joined) | (created)
segmentContentView.addSubview(joinedTableView)
segmentContentView.addSubview(createdTableView)
// /
contentView.addSubview(cycleScrollView)
contentView.addSubview(hotGroupTitleLabel)
contentView.addSubview(hotGroupsCollectionView)
contentView.addSubview(segmentView)
segmentView.addSubview(createdTabLabel)
segmentView.addSubview(joinedTabLabel)
segmentView.addSubview(tabIndicator)
contentView.addSubview(segmentScrollView)
segmentScrollView.addSubview(segmentContentView)
segmentContentView.addSubview(createdTableView)
segmentContentView.addSubview(joinedTableView)
cycleScrollView.isHidden = true
hotGroupTitleLabel.isHidden = true
hotGroupsCollectionView.isHidden = true
addSubview(itineraryPage)
itineraryPage.isHidden = true
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
.heightToWidth(160/375)
navView.layoutChain
topBar.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
mainScrollView.layoutChain
.topToBottomOfView(navBgView)
circleTabLabel.layoutChain
.left(20)
.bottom(10)
.height(28)
itineraryTabLabel.layoutChain
.leftToRightOfView(circleTabLabel, offset: 20)
.centerY(circleTabLabel)
.height(28)
messageBtn.layoutChain
.right(16)
.centerY(circleTabLabel)
.width(28)
.height(28)
circlePage.layoutChain
.topToBottomOfView(topBar)
.edges(excludingEdge: .top)
itineraryPage.layoutChain
.topToBottomOfView(topBar)
.edges(excludingEdge: .top)
mainScrollView.layoutChain
.edges()
contentView.layoutChain
.edges()
.widthToView(mainScrollView)
//
cycleScrollView.layoutChain
.top(10)
.left(15).right(15)
.heightToWidth(100/375)
// /
actionButtonsView.layoutChain
.topToBottomOfView(cycleScrollView, offset: 38)
.edgesHorzontal()
.top(12)
.edgesHorzontal(16)
.height(108)
createGroupBtn.layoutChain
.left(15)
.left()
.edgesVertical()
.widthToView(joinGroupBtn)
.height(40)
joinGroupBtn.layoutChain
.topToView(createGroupBtn)
.bottomToView(createGroupBtn)
.leftToRightOfView(createGroupBtn, offset: 21)
.right(15)
.widthToView(joinGroupBtn)
.leftToRightOfView(createGroupBtn, offset: 12)
.right()
.widthToView(createGroupBtn)
//
hotGroupTitleLabel.layoutChain
.topToBottomOfView(actionButtonsView, offset: 21)
.left(16)
hotGroupsCollectionView.layoutChain
.topToBottomOfView(hotGroupTitleLabel, offset: 8)
listContainer.layoutChain
.topToBottomOfView(actionButtonsView, offset: 16)
.edgesHorzontal()
.height(88)
.bottom()
// Segment
let labelWidth = (UIScreen.main.bounds.width - 60) / 2
segmentView.layoutChain
.topToBottomOfView(hotGroupsCollectionView)
.top()
.edgesHorzontal()
.height(44)
createdTabLabel.layoutChain
joinedTabLabel.layoutChain
.left(30).centerY()
.width(labelWidth)
joinedTabLabel.layoutChain
createdTabLabel.layoutChain
.right(30).centerY()
.width(labelWidth)
tabIndicator.layoutChain
.bottom().centerX(createdTabLabel)
.width(14).height(3)
.topToBottomOfView(joinedTabLabel, offset: 4)
.centerX(joinedTabLabel)
.width(19).height(4)
// segmentScrollView
segmentScrollView.layoutChain
.topToBottomOfView(segmentView)
.edgesHorzontal()
.bottom()
.height(kScreenHeight - kNaviHeight - 44)
.height(kScreenHeight - kNaviHeight - 44 - 12 - 108 - 16)
segmentContentView.layoutChain
.edges()
.heightToView(segmentScrollView)
let svWidth = UIScreen.main.bounds.width
createdTableView.layoutChain
joinedTableView.layoutChain
.top().bottom().left()
.width(svWidth)
joinedTableView.layoutChain
createdTableView.layoutChain
.top().bottom()
.leftToRightOfView(createdTableView)
.leftToRightOfView(joinedTableView)
.width(svWidth)
//
cycleScrollView.layoutChain
.top(0).left(0).width(0).height(0)
hotGroupTitleLabel.layoutChain
.top(0).left(0).width(0).height(0)
hotGroupsCollectionView.layoutChain
.top(0).left(0).width(0).height(0)
configureActionCard(
createGroupBtn,
bgColor: UIColor(hexStr: "#D6F3FF"),
title: "创建圈子",
subtitle: "和朋友一起定位玩"
)
configureActionCard(
joinGroupBtn,
bgColor: UIColor(hexStr: "#E8F8C8"),
title: "加入圈子",
subtitle: "输入邀请码快速加入"
)
}
private func configureActionCard(_ card: UIView, bgColor: UIColor, title: String, subtitle: String) {
card.backgroundColor = bgColor
card.cornerRadius = 16
card.clipsToBounds = true
let titleLab = UILabel()
titleLab.text = title
titleLab.font = .systemFont(ofSize: 16, weight: .bold)
titleLab.textColor = UIColor(hexStr: "#0F2846")
let subLab = UILabel()
subLab.text = subtitle
subLab.font = .systemFont(ofSize: 11, weight: .medium)
subLab.textColor = UIColor(hexStr: "#6B7A90")
let cta = UILabel()
cta.text = "立即体验"
cta.font = .systemFont(ofSize: 12, weight: .semibold)
cta.textColor = UIColor(hexStr: "#0F2846")
cta.textAlignment = .center
cta.backgroundColor = .white
cta.cornerRadius = 12
cta.clipsToBounds = true
card.addSubview(titleLab)
card.addSubview(subLab)
card.addSubview(cta)
titleLab.layoutChain
.top(16)
.left(14)
.right(14)
subLab.layoutChain
.topToBottomOfView(titleLab, offset: 6)
.left(14)
.right(14)
cta.layoutChain
.left(14)
.bottom(14)
.width(72)
.height(24)
}
override func layoutSubviews() {
super.layoutSubviews()
segmentContentView.layoutChain.width(bounds.width * 2)
listContainer.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
listContainer.layer.cornerRadius = 20
listContainer.clipsToBounds = true
}
// MARK: - Nav
@ -161,9 +256,41 @@ class GroupView: UIView {
return iv
}()
// MARK: - Top Bar
lazy var topBar: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
lazy var circleTabLabel: UILabel = {
let label = UILabel()
label.text = "圈子"
label.font = FontManager.youSheBiaoTiHei(32)
label.textColor = UIColor(hexStr: "#0F2846")
label.isUserInteractionEnabled = true
return label
}()
lazy var itineraryTabLabel: UILabel = {
let label = UILabel()
label.text = "行程"
label.font = FontManager.youSheBiaoTiHei(24)
label.textColor = UIColor(hexStr: "#0F2846").withAlphaComponent(0.45)
label.isUserInteractionEnabled = true
return label
}()
lazy var messageBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Home/message"), for: .normal)
btn.isHidden = true
return btn
}()
lazy var navView: BaseNavigationView = {
let nav = BaseNavigationView(title: "圈子")
nav.backBtn.isHidden = true
nav.isHidden = true
return nav
}()
@ -174,6 +301,18 @@ class GroupView: UIView {
return btn
}()
// MARK: - Pages
lazy var circlePage: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
lazy var itineraryPage: GroupItineraryView = {
let v = GroupItineraryView()
return v
}()
// MARK: - Main Scroll
lazy var mainScrollView: PanScrollView = {
let sv = PanScrollView()
@ -190,7 +329,7 @@ class GroupView: UIView {
return v
}()
// MARK: -
// MARK: -
lazy var cycleScrollView: SDCycleScrollView = {
let view = SDCycleScrollView(frame: .zero)
view.backgroundColor = .lightGray
@ -206,34 +345,26 @@ class GroupView: UIView {
return view
}()
// MARK: - /
// MARK: - /
private lazy var actionButtonsView: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
lazy var createGroupBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("创建圈子", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
btn.cornerRadius = 20
return btn
lazy var createGroupBtn: UIView = {
let v = UIView()
v.isUserInteractionEnabled = true
return v
}()
lazy var joinGroupBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("加入圈子", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
btn.cornerRadius = 20
return btn
lazy var joinGroupBtn: UIView = {
let v = UIView()
v.isUserInteractionEnabled = true
return v
}()
// MARK: -
// MARK: -
private lazy var hotGroupTitleLabel: UILabel = {
let label = UILabel()
label.text = "热门酷圈"
@ -255,37 +386,46 @@ class GroupView: UIView {
return cv
}()
// MARK: - Segment
// MARK: - List container
private lazy var listContainer: UIView = {
let v = UIView()
v.backgroundColor = .white
return v
}()
// MARK: - Segment0 =joined1 =created
private(set) lazy var segmentView: UIView = {
let v = UIView()
v.backgroundColor = .white
let line = UIView()
line.backgroundColor = UIColor(hexStr: "#B5B5B5").withAlphaComponent(0.12)
v.addSubview(line)
line.layoutChain
.edgesHorzontal()
.height(1)
.bottom(4)
// v.addSubview(line)
// line.layoutChain
// .edgesHorzontal()
// .height(1)
// .bottom(4)
return v
}()
private(set) lazy var createdTabLabel: UILabel = {
/// joined index 0
private(set) lazy var joinedTabLabel: UILabel = {
let label = UILabel()
label.text = "我创建的圈子"
label.font = .systemFont(ofSize: 15, weight: .semibold)
label.textColor = UIColor(hexStr: "#1A1A1A")
label.text = "全部圈子"
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.textColor = UIColor(hexStr: "#2A3648")
label.textAlignment = .center
label.isUserInteractionEnabled = true
return label
}()
private(set) lazy var joinedTabLabel: UILabel = {
/// created index 1
private(set) lazy var createdTabLabel: UILabel = {
let label = UILabel()
label.text = "加入的圈子"
label.font = .systemFont(ofSize: 15, weight: .medium)
label.textColor = UIColor(hexStr: "#999999")
label.text = "的圈子"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#2A3648")
label.textAlignment = .center
label.isUserInteractionEnabled = true
return label
@ -293,7 +433,7 @@ class GroupView: UIView {
private lazy var tabIndicator: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hexStr: "#16B3FF")
v.backgroundColor = UIColor(hexStr: "#85D8FF")
v.layer.cornerRadius = 2
return v
}()
@ -342,25 +482,40 @@ class GroupView: UIView {
// MARK: - Segment
var currentSegmentIndex: Int = 0
/// index: 0=(joined)1=(created)
func selectSegment(at index: Int) {
currentSegmentIndex = index
let isCreated = index == 0
let isAll = index == 0
UIView.animate(withDuration: 0.2) {
self.createdTabLabel.textColor = isCreated ? UIColor(hexStr: "#1A1A1A") : UIColor(hexStr: "#999999")
self.joinedTabLabel.textColor = isCreated ? UIColor(hexStr: "#999999") : UIColor(hexStr: "#1A1A1A")
self.joinedTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648") : UIColor(hexStr: "#2A3648", alpha: 0.5)
self.joinedTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .heavy : .medium)
self.createdTabLabel.textColor = isAll ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#2A3648")
self.createdTabLabel.font = .systemFont(ofSize: 16, weight: isAll ? .medium : .heavy)
self.tabIndicator.center.x = isCreated
? self.createdTabLabel.center.x
: self.joinedTabLabel.center.x
self.tabIndicator.center.x = isAll
? self.joinedTabLabel.center.x
: self.createdTabLabel.center.x
self.layoutIfNeeded()
}
}
func selectTopTab(at index: Int) {
guard index == 0 || index == 1 else { return }
currentTopTab = index
let isCircle = index == 0
circlePage.isHidden = !isCircle
itineraryPage.isHidden = isCircle
circleTabLabel.font = FontManager.youSheBiaoTiHei(isCircle ? 32 : 24)
itineraryTabLabel.font = FontManager.youSheBiaoTiHei(isCircle ? 24 : 32)
circleTabLabel.textColor = isCircle ? UIColor(hexStr: "#353B4F") : UIColor(hexStr: "#2A3648", alpha: 0.5)
itineraryTabLabel.textColor = isCircle ? UIColor(hexStr: "#2A3648", alpha: 0.5) : UIColor(hexStr: "#353B4F")
}
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
backgroundColor = UIColor(hexStr: "#FAFAFA")
setupUI()
}
@ -586,11 +741,6 @@ final class CircleGroupCell: UITableViewCell {
func configure(_ model: GroupCellData) {
let g = model.groupInfo
// if let faceURL = g.faceURL, !faceURL.isEmpty {
// iconView.dl.setImage(with: faceURL)
// } else {
// iconView.image = UIImage(named: "GroupIcon1")
// }
iconView.image = model.localIcon
nameLabel.text = g.groupName ?? ""
countLabel.text = "\(g.memberCount)个成员"

View File

@ -9,6 +9,7 @@ import RxCocoa
import RxDataSources
import SDCycleScrollView
import OpenIMSDK
import ObjectMapper
final class GroupViewController: BaseViewController {
@ -17,6 +18,10 @@ final class GroupViewController: BaseViewController {
fileprivate var rootView: GroupView!
private let viewModel = GroupViewModel()
///
private var itineraryGroupModel: GroupModel?
private var itinerarySchedules: [ScheduleModel] = []
override func loadView() {
rootView = GroupView(frame: UIScreen.main.bounds)
view = rootView
@ -27,6 +32,7 @@ final class GroupViewController: BaseViewController {
bindViewModel()
reactiveAction()
observeTableViews()
bindItineraryPage()
requestRecommandGroup()
requestGroupInfo()
@ -123,14 +129,30 @@ final class GroupViewController: BaseViewController {
})
.disposed(by: disposeBag)
rootView.createdTabLabel.rx.tapGesture
// Tab(0=joined) / (1=created)
rootView.joinedTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })
.disposed(by: disposeBag)
rootView.joinedTabLabel.rx.tapGesture
rootView.createdTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(1) })
.disposed(by: disposeBag)
// /
rootView.circleTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in
self?.rootView.selectTopTab(at: 0)
})
.disposed(by: disposeBag)
rootView.itineraryTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.rootView.selectTopTab(at: 1)
self.refreshItineraryPage()
})
.disposed(by: disposeBag)
Observable.merge(
rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(),
rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable()
@ -159,12 +181,90 @@ final class GroupViewController: BaseViewController {
.disposed(by: disposeBag)
}
private func bindItineraryPage() {
let page = rootView.itineraryPage
page.onSwitchGroup = { [weak self] in
self?.showSwitchGroupPop()
}
page.onSelectSchedule = { model in
AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()])
}
page.onDeleteSchedule = { [weak self] model in
self?.deleteSchedule(model)
}
}
private func switchToSegment(_ index: Int) {
rootView.selectSegment(at: index)
let offset = CGPoint(x: CGFloat(index) * rootView.segmentScrollView.bounds.width, y: 0)
rootView.segmentScrollView.setContentOffset(offset, animated: false)
}
// MARK: -
private func refreshItineraryPage() {
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
guard let self = self, let model = response.model else { return }
self.itineraryGroupModel = model
let current = model.groups.first(where: { $0.group_key == model.default_group_key })
self.rootView.itineraryPage.updateGroupName(current?.name ?? "")
self.rootView.itineraryPage.reloadMembers(
model.select_group_employee,
selectedId: AppContextManager.shared.userId,
groupKey: model.default_group_key
)
self.requestItinerarySchedules(groupKey: model.default_group_key)
}).disposed(by: disposeBag)
}
private func requestItinerarySchedules(groupKey: String) {
guard !groupKey.isEmpty else {
itinerarySchedules = []
rootView.itineraryPage.reloadSchedules([])
return
}
ItineraryService.groupScheduleList(groupKey: groupKey)
.subscribe(onNext: { [weak self] response in
guard let self = self else { return }
self.itinerarySchedules = response.list
self.rootView.itineraryPage.reloadSchedules(response.list)
})
.disposed(by: disposeBag)
}
private func showSwitchGroupPop() {
guard let groupModel = itineraryGroupModel else {
refreshItineraryPage()
return
}
GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in
guard let self = self, let key = groupKey else { return }
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
.subscribe(onNext: { [weak self] _ in
self?.refreshItineraryPage()
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
})
.disposed(by: self.disposeBag)
}
}
private func deleteSchedule(_ model: ScheduleModel) {
guard !model.id.isEmpty else { return }
DLToast.showLoading()
ItineraryService.delete(id: model.id)
.subscribe(onNext: { [weak self] _ in
DLToast.dismiss()
guard let self = self else { return }
self.itinerarySchedules.removeAll { $0.id == model.id }
self.rootView.itineraryPage.reloadSchedules(self.itinerarySchedules)
}, onError: { _ in
DLToast.dismiss()
})
.disposed(by: disposeBag)
}
// MARK: - tableView
private func observeTableViews() {
rootView.createdTableView.rx.didScroll

View File

@ -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

View File

@ -10,6 +10,7 @@ import ObjectMapper
import RxSwift
import RxCocoa
import Kingfisher
import Photos
class InviteJoinVC: BaseViewController {
@ -25,16 +26,24 @@ class InviteJoinVC: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//
rootView.shareAppBtn.rx.tap.subscribe(onNext: { _ in
SharePopView.show()
//
rootView.shareBtn.rx.tap.subscribe(onNext: { [weak self] in
self?.shareInviteCard()
}).disposed(by: disposeBag)
//
rootView.shareCodeBtn.rx.tap.subscribe(onNext: { [weak self] in
guard let self = self else { return }
//
rootView.downloadBtn.rx.tap.subscribe(onNext: { [weak self] in
self?.downloadInviteCard()
}).disposed(by: disposeBag)
guard let model = groupInModel else { return }
rootView.configure(model: model)
requestOperateGroup(key: model.group_key)
}
// MARK: - Share / Download
private func shareInviteCard() {
guard let config = AppContextManager.shared.systemConfig,
let shareConfig = config.shareConfig else { return }
@ -50,7 +59,7 @@ class InviteJoinVC: BaseViewController {
guard let self = self else { return }
DLToast.dismiss()
guard let screenshot = self.rootView.inviteInfoView.takeScreenshot(),
guard let screenshot = self.rootView.inviteCardView.takeScreenshot(),
let imageData = screenshot.pngData() else {
DLToast.showError(text: "分享失败")
return
@ -87,12 +96,56 @@ class InviteJoinVC: BaseViewController {
} else {
sendMessage(fallbackImage)
}
}).disposed(by: disposeBag)
}
guard let model = groupInModel else { return }
rootView.groupIcon.image = model.groupIcon
rootView.groupNameLab.text = model.name
requestOperateGroup(key: model.group_key)
private func downloadInviteCard() {
guard let screenshot = rootView.inviteCardView.takeScreenshot() else {
DLToast.showError(text: "保存失败")
return
}
let save = { [weak self] in
guard let self = self else { return }
UIImageWriteToSavedPhotosAlbum(
screenshot, self,
#selector(self.image(_:didFinishSavingWithError:contextInfo:)),
nil
)
}
if #available(iOS 14, *) {
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
DispatchQueue.main.async {
if status == .authorized || status == .limited {
save()
} else {
Permission.openAppSetting(
title: "无法访问相册",
message: "请在设置中允许访问相册以保存邀请图片"
)
}
}
}
} else {
Permission.photosAuthorizationStatus { granted in
if granted {
save()
} else {
Permission.openAppSetting(
title: "无法访问相册",
message: "请在设置中允许访问相册以保存邀请图片"
)
}
}
}
}
@objc private func image(_ image: UIImage,
didFinishSavingWithError error: Error?,
contextInfo: UnsafeRawPointer) {
if error != nil {
DLToast.showError(text: "保存失败")
} else {
DLToast.showSuccess(text: "已保存")
}
}
// MARK: - API
@ -103,8 +156,8 @@ class InviteJoinVC: BaseViewController {
let data = response.data,
let code = data["share_code"] as? String else { return }
self.rootView.inviteCodeLab.text = code
self.rootView.qrCodeImgView.image = code.createQRCode(size: self.rootView.qrCodeImgView.dl.size)
}, onError: { (error) in
self.rootView.qrCodeImgView.image = code.createQRCode(size: CGSize(width: 120, height: 120))
}, onError: { _ in
DLToast.show(text: "获取错误,请重试") {
AppRouter.shared.popOrDismiss()
}
@ -120,5 +173,4 @@ class InviteJoinVC: BaseViewController {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -13,154 +13,220 @@ class InviteJoinView: UIView {
var disposeBag = DisposeBag()
private func setupRx() {
func configure(model: GroupInfoModel) {
groupIcon.image = model.groupIcon
groupNameLab.text = model.name
groupDescLab.text = model.description
groupDescLab.isHidden = model.description.isEmpty
inviteCodeView.rx.tapGesture.subscribe { _ in
guard let code = self.inviteCodeLab.text else { return }
tagStack.arrangedSubviews.forEach {
tagStack.removeArrangedSubview($0)
$0.removeFromSuperview()
}
let labels = Array(model.labels.prefix(2))
tagStack.isHidden = labels.isEmpty
for text in labels {
tagStack.addArrangedSubview(Self.makeTagLabel(text))
}
}
private static func makeTagLabel(_ text: String) -> UILabel {
let label = InviteTagLabel()
label.text = text
label.textColor = UIColor(hexStr: "#D5923F")
label.font = .systemFont(ofSize: 10, weight: .medium)
label.backgroundColor = UIColor(hexStr: "#FFF9DD")
label.cornerRadius = 4
label.clipsToBounds = true
label.textAlignment = .center
return label
}
private func setupRx() {
inviteCodeView.rx.tapGesture.subscribe { [weak self] _ in
guard let code = self?.inviteCodeLab.text, !code.isEmpty else { return }
UIPasteboard.general.string = code
DLToast.showSuccess(text: "已复制")
}.disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navView)
addSubview(groupIcon)
addSubview(groupNameLab)
addSubview(inviteInfoView)
inviteInfoView.addSubview(inviteInfoTitleLab)
inviteInfoView.addSubview(inviteTipsLab)
inviteInfoView.addSubview(inviteCodeView)
addSubview(inviteCardView)
inviteCardView.addSubview(cardBgImg)
inviteCardView.addSubview(groupView)
inviteCardView.addSubview(inviteInfoTitleLab)
inviteCardView.addSubview(inviteCodeView)
inviteCodeView.addSubview(inviteCodeTipsLab)
inviteCodeView.addSubview(inviteCodeLab)
inviteInfoView.addSubview(scanTipsLab)
inviteInfoView.addSubview(qrCodeView)
qrCodeView.addSubview(qrCodeBgImg)
inviteCardView.addSubview(inviteTipsLab)
inviteCardView.addSubview(scanTipsLab)
inviteCardView.addSubview(qrCodeView)
qrCodeView.addSubview(qrCodeImgView)
addSubview(shareAppBtn)
addSubview(shareCodeBtn)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
addSubview(shareBtn)
addSubview(downloadBtn)
navView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
groupIcon.layoutChain
.topToBottomOfView(navView, offset: 24)
.width(80)
.height(80)
.centerX()
shareBtn.layoutChain
.left(30)
.bottom(kSafeBottomMargin + 12)
.height(56)
.width(110)
groupNameLab.layoutChain
.topToBottomOfView(groupIcon, offset: 4)
.centerX()
downloadBtn.layoutChain
.leftToRightOfView(shareBtn, offset: 16)
.right(30)
.bottomToView(shareBtn)
.heightToView(shareBtn)
shareAppBtn.layoutChain
.left(44)
.bottom(kSafeBottomMargin + 10)
.widthToView(shareCodeBtn)
.height(50)
inviteCardView.layoutChain
.topToBottomOfView(navView, offset: 42)
.edgesHorzontal(15)
.heightToWidth(481/345)
shareCodeBtn.layoutChain
.leftToRightOfView(shareAppBtn, offset: 20)
.right(44)
.bottomToView(shareAppBtn)
.widthToView(shareAppBtn)
.heightToView(shareAppBtn)
cardBgImg.layoutChain.edges()
inviteInfoView.layoutChain
.topToBottomOfView(groupIcon, offset: 50)
.edgesHorzontal(30)
// .bottomToTopOfView(shareAppBtn, offset: -32)
groupView.layoutChain
.top(16)
.edgesHorzontal(16)
inviteInfoTitleLab.layoutChain
.top(20)
.centerX()
inviteTipsLab.layoutChain
.topToBottomOfView(inviteInfoTitleLab, offset: 9)
.topToBottomOfView(groupIcon, offset: 28)
.centerX()
inviteCodeView.layoutChain
.topToBottomOfView(inviteTipsLab, offset: 10)
.topToBottomOfView(inviteInfoTitleLab, offset: 12)
.centerX()
.width(180)
.height(50)
.width(200)
.height(56)
inviteCodeTipsLab.layoutChain
.top(10)
.top(8)
.centerX()
inviteCodeLab.layoutChain
.topToBottomOfView(inviteCodeTipsLab)
.topToBottomOfView(inviteCodeTipsLab, offset: 7)
.centerX()
scanTipsLab.layoutChain
.topToBottomOfView(inviteCodeView, offset: 20)
inviteTipsLab.layoutChain
.topToBottomOfView(inviteCodeView, offset: 10)
.centerX()
qrCodeView.layoutChain
.topToBottomOfView(scanTipsLab, offset: 15)
.bottom(42)
.centerX()
.widthToHeight(1)
.bottom(20)
qrCodeBgImg.layoutChain.edges()
.width(138)
.height(138)
qrCodeImgView.layoutChain
.edgesHorzontal(4)
.edgesVertical(4)
}
.edges(all: 18)
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
scanTipsLab.layoutChain
.bottomToTopOfView(qrCodeView, offset: -8)
.centerX()
}
lazy var navView: BaseNavigationView = {
let nav = BaseNavigationView(title: "邀请成员")
nav.backBtn.setImage(UIImage(named: "Group/invite_back"), for: .normal)
nav.backBtn.layoutChain.width(36).height(36)
return nav
}()
/// /
lazy var inviteCardView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var cardBgImg: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Group/invite_card_bg")
view.contentMode = .scaleAspectFit
return view
}()
lazy var groupView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EEF7FB")
view.cornerRadius = 18
view.addSubview(groupIcon)
groupIcon.layoutChain
.left(12)
.edgesVertical(9)
.width(52)
.heightToWidth(1)
view.addSubview(groupNameLab)
groupNameLab.layoutChain
.topToView(groupIcon, offset: 2)
.leftToRightOfView(groupIcon, offset: 10)
view.addSubview(tagStack)
tagStack.layoutChain
.leftToRightOfView(groupNameLab, offset: 6)
.centerY(groupNameLab)
.right(12, relation: .greaterThanOrEqual)
view.addSubview(groupDescLab)
groupDescLab.layoutChain
.topToBottomOfView(groupNameLab, offset: 14)
.leftToView(groupNameLab)
.right(24)
return view
}()
lazy var groupIcon: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.cornerRadius = 40
view.contentMode = .scaleAspectFill
view.cornerRadius = 10
view.clipsToBounds = true
return view
}()
lazy var groupNameLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor(hexStr: "#293445")
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}()
lazy var inviteInfoView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
view.cornerRadius = 20
lazy var tagStack: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.alignment = .center
view.spacing = 4
view.setContentCompressionResistancePriority(.required, for: .horizontal)
return view
}()
lazy var groupDescLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .regular)
label.textColor = UIColor(hexStr: "#999999")
return label
}()
lazy var inviteInfoTitleLab: UILabel = {
let label = UILabel()
label.text = "将此邀请码分享给加入圈子的人"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#353B4F")
return label
}()
lazy var inviteTipsLab: UILabel = {
let label = UILabel()
label.text = "此邀请码有效期为24小时"
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textColor = UIColor(hexStr: "#16B3FF")
label.font = .systemFont(ofSize: 12, weight: .regular)
label.textColor = UIColor(hexStr: "#00ADFE")
return label
}()
@ -169,78 +235,80 @@ class InviteJoinView: UIView {
view.borderColor = UIColor(hexStr: "#CCCCCC")
view.borderWidth = 1
view.cornerRadius = 10
view.backgroundColor = .white
return view
}()
lazy var inviteCodeTipsLab: UILabel = {
let label = UILabel()
label.text = "点击复制邀请码"
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textColor = ThemeManager.shared.color.contentColor
label.font = .systemFont(ofSize: 11, weight: .medium)
label.textColor = UIColor(hexStr: "#999999")
return label
}()
lazy var inviteCodeLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor(hexStr: "#00ADFE")
return label
}()
lazy var scanTipsLab: UILabel = {
let label = UILabel()
label.text = "— 或扫描以下二维码"
label.font = .systemFont(ofSize: 10)
label.text = "扫描以下二维码"
label.font = .systemFont(ofSize: 12, weight: .regular)
label.textColor = ThemeManager.shared.color.contentColor
return label
}()
lazy var qrCodeView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var qrCodeBgImg: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Group/scan_bg")
view.backgroundColor = .white
view.borderWidth = 3
view.borderColor = UIColor(hexStr: "#EAEEEF")
view.cornerRadius = 34
return view
}()
lazy var qrCodeImgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.contentMode = .scaleAspectFill
view.cornerRadius = 10
view.backgroundColor = .white
view.contentMode = .scaleAspectFit
view.cornerRadius = 8
view.clipsToBounds = true
return view
}()
lazy var shareAppBtn: UIButton = {
lazy var shareBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("分享APP", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.setTitle("分享", for: .normal)
btn.titleLabel?.font = FontManager.boboBold(18)
btn.setTitleColor(UIColor(hexStr: "#00ADFE"), for: .normal)
btn.backgroundColor = .white
btn.borderWidth = 1
btn.borderColor = UIColor(hexStr: "#16B3FF")
btn.cornerRadius = 25
btn.borderColor = UIColor(hexStr: "#00ADFE")
btn.cornerRadius = 20
return btn
}()
lazy var shareCodeBtn: UIButton = {
lazy var downloadBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("分享邀请码", for: .normal)
btn.setTitle("下载图片", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.titleLabel?.font = FontManager.boboBold(18)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25
btn.cornerRadius = 20
btn.clipsToBounds = true
return btn
}()
///
var inviteInfoView: UIView { inviteCardView }
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
backgroundColor = .clear
setupUI()
setupRx()
}
@ -248,4 +316,29 @@ class InviteJoinView: UIView {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
setGradientLayer(
frame: bounds,
startPoint: CGPoint(x: 0.5, y: 0),
endPoint: CGPoint(x: 0.5, y: 1),
colors: [UIColor(hexStr: "#A1E8FF"), UIColor(hexStr: "#E4F8FF")],
locations: [0, 1]
)
}
}
private class InviteTagLabel: UILabel {
private let inset = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: inset))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + inset.left + inset.right,
height: size.height + inset.top + inset.bottom)
}
}

View File

@ -11,19 +11,23 @@ class GroupListPopView: UIView {
private static let shared = GroupListPopView(frame: CGRect(origin: .zero, size: kScreenSize))
private static let rowHeight: CGFloat = 72
private static let maxVisibleRows = 3
private static let primaryBlue = UIColor(hexStr: "#16B3FF")
private static let cardBg = UIColor(hexStr: "#F5FBFF")
private static let tagBg = UIColor(hexStr: "#FFF9DD")
private static let tagText = UIColor(hexStr: "#D5923F")
private var groupModel: GroupModel? {
didSet {
guard let model = groupModel else { return }
groupNameLab.text = model.groups.first(where: { $0.group_key == model.default_group_key })?.name ?? ""
defaultGroupKey = model.default_group_key
selectedGroupKey = model.default_group_key
groupList = model.groups
let viewHeight = CGFloat(model.groups.prefix(5).count * 68)
tableView.layoutChain.height(viewHeight, relation: .greaterThanOrEqual)
updateTableHeight()
}
}
private var defaultGroupKey: String = ""
private var selectedGroupKey: String = ""
private var groupList: [GroupInfoModel] = [] {
didSet {
@ -34,18 +38,16 @@ class GroupListPopView: UIView {
///
private var completion: ((String?) -> Void)?
@objc func tap() {
@objc private func tapMask() {
completion?(nil)
}
@objc func createAction(button: UIButton) {
@objc private func cancelAction() {
completion?(nil)
AppRouter.push(Route.createGroup)
}
@objc func joinAction(button: UIButton) {
completion?(nil)
AppRouter.push(Route.joinGroup)
@objc private func confirmAction() {
completion?(selectedGroupKey.isEmpty ? nil : selectedGroupKey)
}
private lazy var bgView: UIView = {
@ -55,79 +57,63 @@ class GroupListPopView: UIView {
return view
}()
//
lazy var groupView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 18
return view
}()
lazy var groupIconView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Home/group")
view.backgroundColor = .clear
view.contentMode = .scaleAspectFill
return view
}()
lazy var groupNameLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#0F2846")
label.textAlignment = .center
return label
}()
lazy var groupArrowIconView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Home/arrow_down")
view.backgroundColor = .clear
view.contentMode = .scaleAspectFill
return view
}()
lazy var infoView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 10
view.backgroundColor = .clear
let bg = UIView()
bg.backgroundColor = UIColor(hexStr: "#FDFDFD")
view.addSubview(bg)
bg.layoutChain
.top(80)
.edges(excludingEdge: .top)
return view
}()
lazy var headerImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Home/group_list_header")
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
return view
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.backgroundColor = .white
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 68
tableView.rowHeight = Self.rowHeight
tableView.estimatedRowHeight = Self.rowHeight
tableView.showsVerticalScrollIndicator = false
tableView.bounces = false
tableView.register(GroupListCell.self)
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
tableView.dataSource = self
tableView.delegate = self
return tableView
}()
lazy var createGroupBtn: UIButton = {
lazy var cancelBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("创建圈子", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
btn.cornerRadius = 22
btn.addTarget(self, action: #selector(createAction), for: .touchUpInside)
btn.setTitle("取消", for: .normal)
btn.setTitleColor(Self.primaryBlue, for: .normal)
btn.titleLabel?.font = FontManager.boboBold(18)
btn.backgroundColor = .white
btn.cornerRadius = 16
btn.layer.borderWidth = 1
btn.layer.borderColor = Self.primaryBlue.cgColor
btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
return btn
}()
lazy var joinGroupBtn: UIButton = {
lazy var confirmBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("加入圈子", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/gradient_bg"), for: .normal)
btn.cornerRadius = 22
btn.addTarget(self, action: #selector(joinAction), for: .touchUpInside)
btn.setTitle("确定", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = FontManager.boboBold(18)
btn.backgroundColor = Self.primaryBlue
btn.cornerRadius = 16
btn.addTarget(self, action: #selector(confirmAction), for: .touchUpInside)
return btn
}()
@ -137,61 +123,41 @@ class GroupListPopView: UIView {
backgroundColor = .clear
addSubview(bgView)
bgView.addSubview(infoView)
infoView.addSubview(headerImageView)
infoView.addSubview(tableView)
infoView.addSubview(createGroupBtn)
infoView.addSubview(joinGroupBtn)
infoView.addSubview(cancelBtn)
infoView.addSubview(confirmBtn)
bgView.addSubview(groupView)
groupView.addSubview(groupIconView)
groupView.addSubview(groupNameLab)
groupView.addSubview(groupArrowIconView)
groupView.layoutChain
.top(59)
.height(36)
.centerX()
.width(185, relation: .greaterThanOrEqual)
groupIconView.layoutChain
.left(11)
.centerY()
.width(30)
.height(30)
groupArrowIconView.layoutChain
.right(15)
.centerY()
.width(15)
.height(8.5)
groupNameLab.layoutChain
.edgesVertical()
.leftToRightOfView(groupIconView)
.rightToLeftOfView(groupArrowIconView)
bgView.layoutChain.edges()
infoView.layoutChain
.topToBottomOfView(groupView, offset: 20)
.edgesHorzontal(15)
.bottom()
.edgesHorzontal()
createGroupBtn.layoutChain
.left(15)
.bottom(20)
.widthToView(joinGroupBtn)
.height(44)
headerImageView.layoutChain
.top()
.edgesHorzontal()
.heightToWidth(127/375)
joinGroupBtn.layoutChain
.leftToRightOfView(createGroupBtn, offset: 7)
.right(15)
.bottom(20)
cancelBtn.layoutChain
.left(20)
.bottom(20 + kSafeBottomMargin)
.height(44)
.widthToView(confirmBtn)
confirmBtn.layoutChain
.leftToRightOfView(cancelBtn, offset: 12)
.right(20)
.bottomToView(cancelBtn)
.height(44)
.widthToView(joinGroupBtn)
tableView.layoutChain
.edges(excludingEdge: .bottom)
.height(78, relation: .greaterThanOrEqual)
.bottomToTopOfView(createGroupBtn, offset: -17)
.topToBottomOfView(headerImageView, offset: -20)
.edgesHorzontal(12)
.height(Self.rowHeight)
.bottomToTopOfView(cancelBtn, offset: -16)
let tap = UITapGestureRecognizer(target: self, action: #selector(tap))
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask))
tap.delegate = self
addGestureRecognizer(tap)
}
@ -200,61 +166,66 @@ class GroupListPopView: UIView {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
private func updateTableHeight() {
let visible = min(groupList.count, Self.maxVisibleRows)
let height = CGFloat(visible) * Self.rowHeight
tableView.layoutChain.height(height)
tableView.isScrollEnabled = groupList.count > Self.maxVisibleRows
tableView.reloadData()
}
private func slideOffTransform() -> CGAffineTransform {
layoutIfNeeded()
let offset = infoView.bounds.height// + 20 + kSafeBottomMargin
return CGAffineTransform(translationX: 0, y: max(offset, kScreenHeight * 0.5))
}
}
// MARK: - Public
extension GroupListPopView {
///
/// - Parameters:
/// - start:
static func show(start: CGPoint,
groupModel: GroupModel,
/// window
static func show(groupModel: GroupModel,
completion: @escaping ((String?) -> Void)) {
guard let superView = kKeyWindow else {
return
guard let superView = kKeyWindow else { return }
let pop = GroupListPopView.shared
if pop.superview != nil {
pop.removeFromSuperview()
}
if GroupListPopView.shared.superview != nil {
GroupListPopView.shared.removeFromSuperview()
GroupListPopView.shared.bgView.frame = .zero
}
GroupListPopView.shared.groupModel = groupModel
GroupListPopView.shared.bgView.alpha = 1
GroupListPopView.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
superView.addSubview(GroupListPopView.shared)
superView.bringSubviewToFront(GroupListPopView.shared)
GroupListPopView.shared.infoView.alpha = 0
GroupListPopView.shared.completion = { (text) in
pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
pop.groupModel = groupModel
pop.completion = { text in
completion(text)
GroupListPopView.dismiss()
}
UIView.animate(withDuration: 0.25) {
GroupListPopView.shared.infoView.alpha = 1
// GroupListPopView.shared.infoView.dl.y = start.y
pop.bgView.alpha = 0
superView.addSubview(pop)
superView.bringSubviewToFront(pop)
pop.layoutIfNeeded()
pop.infoView.transform = pop.slideOffTransform()
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
pop.bgView.alpha = 1
pop.infoView.transform = .identity
}
}
///
///
static func dismiss() {
guard GroupListPopView.shared.superview != nil else { return }
let viewHeight = GroupListPopView.shared.infoView.frame.height
UIView.animate(withDuration: 0.15) {
GroupListPopView.shared.infoView.alpha = 0
// GroupListPopView.shared.infoView.dl.y = -viewHeight
}
let pop = GroupListPopView.shared
guard pop.superview != nil else { return }
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
GroupListPopView.shared.bgView.alpha = 0
pop.bgView.alpha = 0
pop.infoView.transform = pop.slideOffTransform()
} completion: { _ in
GroupListPopView.shared.removeFromSuperview()
pop.infoView.transform = .identity
pop.removeFromSuperview()
}
}
}
// MARK: - UIGestureRecognizerDelegate
extension GroupListPopView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
@ -273,105 +244,178 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model: groupList[indexPath.row],
isSelected: defaultGroupKey == groupList[indexPath.row].group_key)
let model = groupList[indexPath.row]
cell.configure(model: model, isSelected: selectedGroupKey == model.group_key)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
completion?(groupList[indexPath.row].group_key)
selectedGroupKey = groupList[indexPath.row].group_key
tableView.reloadData()
}
}
// MARK: - GroupListPopCell
// MARK: - GroupListCell
class GroupListCell: UITableViewCell {
func configure(model: GroupInfoModel, isSelected: Bool) {
avaterImgView.image = model.groupIcon
nameLab.text = model.name
bgView.isHidden = !isSelected
countLab.text = "\(model.people_no)成员"
tagStack.arrangedSubviews.forEach {
tagStack.removeArrangedSubview($0)
$0.removeFromSuperview()
}
let labels = Array(model.labels.prefix(2))
tagStack.isHidden = labels.isEmpty
for text in labels {
tagStack.addArrangedSubview(Self.makeTagLabel(text))
}
cardView.backgroundColor = isSelected ? GroupListPopView.cardBgColor : .white
cardView.layer.borderWidth = isSelected ? 1 : 0
cardView.layer.borderColor = GroupListPopView.primaryBlueColor.cgColor
checkIcon.image = isSelected
? UIImage(systemName: "checkmark.circle.fill")
: UIImage(systemName: "circle")
checkIcon.tintColor = isSelected
? GroupListPopView.primaryBlueColor
: UIColor(hexStr: "#D0D0D0")
}
private static func makeTagLabel(_ text: String) -> UILabel {
let label = PaddingLabel()
label.text = text
label.textColor = GroupListPopView.tagTextColor
label.font = .systemFont(ofSize: 10, weight: .medium)
label.backgroundColor = GroupListPopView.tagBgColor
label.cornerRadius = 4
label.clipsToBounds = true
label.textAlignment = .center
return label
}
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
setupSubviews()
}
private func setupSubviews() {
contentView.addSubview(bgView)
contentView.addSubview(avaterImgView)
contentView.addSubview(nameLab)
contentView.addSubview(cardView)
cardView.addSubview(avaterImgView)
cardView.addSubview(nameLab)
cardView.addSubview(tagStack)
cardView.addSubview(countLab)
cardView.addSubview(checkIcon)
bgView.layoutChain.edges()
cardView.layoutChain
.edgesVertical(4)
.edgesHorzontal(4)
avaterImgView.layoutChain
.edgesVertical(14)
.left(15)
.width(40)
.height(40)
.left(12)
.centerY()
.width(44)
.height(44)
checkIcon.layoutChain
.right(12)
.centerY()
.width(22)
.height(22)
nameLab.layoutChain
.centerY(avaterImgView)
.leftToRightOfView(avaterImgView, offset: 11)
.right(100, relation: .greaterThanOrEqual)
.topToView(avaterImgView, offset: 2)
.leftToRightOfView(avaterImgView, offset: 10)
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
tagStack.layoutChain
.leftToRightOfView(nameLab, offset: 6)
.centerY(nameLab)
.rightToLeftOfView(checkIcon, offset: -8, relation: .lessThanOrEqual)
countLab.layoutChain
.topToBottomOfView(nameLab, offset: 4)
.leftToView(nameLab)
.rightToLeftOfView(checkIcon, offset: -8)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
lazy var bgView: UIView = {
lazy var cardView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
let lineView = UIView()
lineView.backgroundColor = UIColor(hexStr: "#16B3FF")
view.addSubview(lineView)
lineView.layoutChain
.left()
.edgesVertical()
.width(6)
let selectedIcon = UIImageView()
selectedIcon.image = UIImage(named: "Group/selected")
view.addSubview(selectedIcon)
selectedIcon.layoutChain
.right(14)
.centerY()
.width(24)
.height(24)
view.isHidden = true
view.backgroundColor = .white
view.cornerRadius = 12
view.clipsToBounds = true
return view
}()
lazy var avaterImgView: UIImageView = {
let view = UIImageView()
// view.backgroundColor = .lightGray
view.contentMode = .scaleAspectFill
// view.cornerRadius = 20
view.cornerRadius = 8
view.clipsToBounds = true
return view
}()
lazy var nameLab: UILabel = {
let label = UILabel()
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textColor = UIColor(hexStr: "#353B4F")
label.font = .systemFont(ofSize: 15, weight: .medium)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}()
lazy var tagStack: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.alignment = .center
view.spacing = 4
view.setContentCompressionResistancePriority(.required, for: .horizontal)
return view
}()
lazy var countLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#999999")
label.font = .systemFont(ofSize: 12, weight: .regular)
return label
}()
lazy var checkIcon: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFit
view.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
return view
}()
}
// MARK: - Color accessors for cell (file-private via static)
private extension GroupListPopView {
static var primaryBlueColor: UIColor { primaryBlue }
static var cardBgColor: UIColor { cardBg }
static var tagBgColor: UIColor { tagBg }
static var tagTextColor: UIColor { tagText }
}
///
private class PaddingLabel: UILabel {
private let inset = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6)
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: inset))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + inset.left + inset.right,
height: size.height + inset.top + inset.bottom)
}
}

Some files were not shown because too many files have changed in this diff Show More