- 改版(登录100%、首页50%)

This commit is contained in:
linshujie 2026-08-04 18:32:36 +08:00
parent 5f233f4ede
commit 69e38f19fe
33 changed files with 1428 additions and 451 deletions

View File

@ -294,6 +294,8 @@
30EFF3E72FDAA93D00EB35D4 /* PrivacyPolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3E62FDAA93D00EB35D4 /* PrivacyPolicyView.swift */; };
30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; };
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 */; };
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 */; };
@ -601,6 +603,8 @@
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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>"; };
@ -1003,9 +1007,11 @@
isa = PBXGroup;
children = (
305A76272FCA8C7000227D26 /* GroupMemberView.swift */,
305A76282FCA8C7000227D26 /* HomeView.swift */,
305A76292FCA8C7000227D26 /* HomeViewController.swift */,
305A798B2FCAB99300227D26 /* HomeViewModel.swift */,
305A76282FCA8C7000227D26 /* HomeView.swift */,
55B2179030217D6600784774 /* HomeView2.swift */,
55B2179230218CE100784774 /* GroupMemberView2.swift */,
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */,
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */,
30D87CDC2FDFF07500E958FD /* InteractionView.swift */,
@ -1915,6 +1921,7 @@
30EFF3E02FDA9CE300EB35D4 /* EmergencyContactModel.swift in Sources */,
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
55B2179130217D6600784774 /* HomeView2.swift in Sources */,
30B819F22FF3CE3C00FAB693 /* ItineraryTraceView.swift in Sources */,
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
@ -1934,6 +1941,7 @@
3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */,
30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */,
305A76C22FCA8C7000227D26 /* BaseViewModel.swift in Sources */,
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */,
30A87A622FEE724D0095E7C6 /* CreateBubbleTiemView.swift in Sources */,
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */,
30A87A642FEE75520095E7C6 /* CreateBubbleTipsView.swift in Sources */,
@ -2155,6 +2163,7 @@
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "为了保存图片到您的相册,请允许添加照片,谢谢。";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的相册,以便您可以发送照片。";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIBackgroundModes = location;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
@ -2208,6 +2217,7 @@
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "为了保存图片到您的相册,请允许添加照片,谢谢。";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "我们需要访问您的相册,以便您可以发送照片。";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UIBackgroundModes = location;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "vuesaxboldmouse@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "vuesaxboldmouse@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" : "invite@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "invite@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -59,16 +59,12 @@ public let kScreenWidth = UIScreen.main.bounds.size.width
public let kScreenHeight = UIScreen.main.bounds.size.height
///
public var kStatusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
let statusBarHeight = kKeyWindow?
.windowScene?
.statusBarManager?
.statusBarFrame
.height ?? 0
return statusBarHeight
} else {
return UIApplication.shared.statusBarFrame.height
}
}
/// +
public let kNaviHeight = 44 + kStatusBarHeight

View File

@ -69,5 +69,9 @@
<string>douyu.otf</string>
<string>zihunbiantaoti-r.ttf</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
</dict>
</plist>

View File

@ -58,9 +58,9 @@ final class MainTabBarController: UITabBarController {
view.addSubview(customTabBar)
customTabBar.delegate = self
// ""index=1
// ""index=1""index=2
customTabBar.shouldSelectTab = { index in
if index == 1 || index == 2, AppContextManager.shared.isGuest {
if (index == 1 || index == 2), AppContextManager.shared.isGuest {
AppRouter.push(Route.login)
return false
}
@ -98,11 +98,6 @@ final class MainTabBarController: UITabBarController {
// MARK: - QuickLocationTabBarDelegate
extension MainTabBarController: QuickLocationTabBarDelegate {
func tabBar(_ tabBar: QuickLocationTabBar, didSelectTabAt index: Int) {
// ""index=1
if index == 1 || index == 2, AppContextManager.shared.isGuest {
AppRouter.push(Route.login)
return
}
selectedIndex = index
}
}

View File

@ -24,9 +24,9 @@ final class QuickLocationTabBar: UIView {
}
// MARK: - Design Constants (in iOS points, from Lanhu @2x 2)
private let barContentHeight: CGFloat = 56
private let containerCornerRadius: CGFloat = 28
private let horizontalInset: CGFloat = 18
private let barContentHeight: CGFloat = 49
private let containerCornerRadius: CGFloat = 0
private let horizontalInset: CGFloat = 0
private let iconSize: CGFloat = 32
private let iconLabelSpacing: CGFloat = 4
private let labelHeight: CGFloat = 11
@ -46,6 +46,7 @@ final class QuickLocationTabBar: UIView {
private let items: [TabItem]
private var tabViews: [UIView] = []
private let containerView = UIView()
private let topSeparatorView = UIView()
private(set) var selectedIndex: Int = 0
@ -72,12 +73,11 @@ final class QuickLocationTabBar: UIView {
containerView.backgroundColor = .white
containerView.layer.cornerRadius = containerCornerRadius
containerView.layer.shadowColor = UIColor(red: 0.059, green: 0.157, blue: 0.275, alpha: 0.1).cgColor
containerView.layer.shadowOffset = .zero
containerView.layer.shadowRadius = 8
containerView.layer.shadowOpacity = 1
addSubview(containerView)
topSeparatorView.backgroundColor = UIColor(hexStr: "#E8E8E8")
addSubview(topSeparatorView)
for (index, item) in items.enumerated() {
let tabView = makeTabView(item: item, index: index)
containerView.addSubview(tabView)
@ -86,11 +86,18 @@ final class QuickLocationTabBar: UIView {
containerView.translatesAutoresizingMaskIntoConstraints = false
topSeparatorView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topSeparatorView.leadingAnchor.constraint(equalTo: leadingAnchor),
topSeparatorView.trailingAnchor.constraint(equalTo: trailingAnchor),
topSeparatorView.topAnchor.constraint(equalTo: topAnchor),
topSeparatorView.heightAnchor.constraint(equalToConstant: 1.0 / UIScreen.main.scale),
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset),
containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -kSafeBottomMargin),
containerView.heightAnchor.constraint(equalToConstant: barContentHeight)
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
containerView.heightAnchor.constraint(equalToConstant: barContentHeight + kSafeBottomMargin)
])
}
@ -157,7 +164,7 @@ final class QuickLocationTabBar: UIView {
guard containerWidth > 0, !tabViews.isEmpty else { return }
let tabWidth = containerWidth / CGFloat(tabViews.count)
let tabHeight = containerView.bounds.height
let tabHeight = containerView.bounds.height - kSafeBottomMargin
let totalHeight = iconSize + iconLabelSpacing + labelHeight
let startY = (tabHeight - totalHeight) / 2

View File

@ -657,13 +657,13 @@ class GroupMemberListCell: UICollectionViewCell {
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
selectedBgView.isHidden = !isSelected
//
if AppContextManager.shared.vip > 1, model.is_online {
batteryInfoView.isHidden = model.battery.int == 0
// 16
let batteryPercent = min(CGFloat(model.battery.int), 100)
batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0)
batteryLab.text = "\(model.battery)%"
}
// if AppContextManager.shared.vip > 1, model.is_online {
// batteryInfoView.isHidden = model.battery.int == 0
// // 16
// let batteryPercent = min(CGFloat(model.battery.int), 100)
// batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0)
// batteryLab.text = "\(model.battery)%"
// }
}
private func setupSubviews() {
@ -684,8 +684,8 @@ class GroupMemberListCell: UICollectionViewCell {
selectedBgView.layoutChain.edges()
avaterImgView.layoutChain
.top(11)
.edgesHorzontal(10)
.top(9)
.edgesHorzontal(5.5)
.heightToWidth(1)
batteryInfoView.layoutChain

View File

@ -161,7 +161,11 @@ class GroupView: UIView {
return iv
}()
lazy var navView = BaseNavigationView(title: "圈子")
lazy var navView: BaseNavigationView = {
let nav = BaseNavigationView(title: "圈子")
nav.backBtn.isHidden = true
return nav
}()
lazy var scanBtn: UIButton = {
let btn = UIButton(type: .custom)

View File

@ -5,7 +5,6 @@
// Created by on 2026/5/29.
//
import UIKit
import UIKit
import RxSwift
import RxCocoa

View File

@ -0,0 +1,352 @@
//
// GroupMemberView2.swift
// QuickLocation
//
// Created by on 2026/8/4.
//
import UIKit
import RxSwift
import RxCocoa
class GroupMemberView2: UIView {
private var navBarHeightConstraint: NSLayoutConstraint?
/// kNaviHeight/dismiss 0
func setNavBarExpanded(_ expanded: Bool) {
navBarHeightConstraint?.constant = expanded ? kNaviHeight : 0
}
func setupMemberInfo(_ model: GroupMemberModel, isOwner: Bool) {
memberNameLab.text = model.nick_name
ownView.isHidden = !isOwner
statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8")
statusLab.text = model.is_online ? "在线" : "离线"
locationLab.text = model.lastLocation
}
private func setupRx() {
}
private func setupUI() {
addSubview(headerBgView)
addSubview(navBarView)
addSubview(scrollView)
headerBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(0)
navBarHeightConstraint = navBarView.jh_constraint(
.height, toAttribute: .notAnAttribute, otherView: nil, relation: .equal
)
scrollView.layoutChain
.topToBottomOfView(navBarView)
.edges(excludingEdge: .top)
scrollView.isScrollEnabled = false
let lineView = UIView()
lineView.backgroundColor = .black.withAlphaComponent(0.2)
lineView.cornerRadius = 2
addSubview(lineView)
lineView.layoutChain
.top(9)
.centerX()
.width(40)
.height(4)
}
lazy var headerBgView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Home/group_member_bg2")
view.backgroundColor = .clear
view.contentMode = .scaleAspectFill
return view
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.clipsToBounds = true
return view
}()
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.backgroundColor = .clear
view.showsHorizontalScrollIndicator = false
view.bounces = false
view.addSubview(scrollContentView)
scrollContentView.layoutChain.edges().widthToView(view)
scrollContentView.addSubview(memberView)
memberView.layoutChain
.edges()
return view
}()
lazy var scrollContentView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
// MARK: -
lazy var memberView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let icon = UIImageView()
icon.image = UIImage(named: "Home/group_name_icon")
view.addSubview(icon)
icon.layoutChain
.top(23)
.left(15)
.width(18)
.heightToWidth(1)
view.addSubview(groupNameLab)
groupNameLab.layoutChain
.leftToRightOfView(icon, offset: 6)
.centerY(icon)
view.addSubview(changeGroupBtn)
changeGroupBtn.layoutChain
.centerY(icon)
.right(15)
.height(20)
let memberView = UIView()
memberView.backgroundColor = .white
memberView.cornerRadius = 20
memberView.addSubview(memberCV)
view.addSubview(memberView)
memberView.layoutChain
.topToBottomOfView(icon, offset: 12)
.edgesHorzontal(15)
.height(100)
memberCV.layoutChain.edges(all: 5)
view.addSubview(memberInfoView)
memberInfoView.layoutChain
.topToBottomOfView(memberView, offset: 12)
.edgesHorzontal(15)
.height(150)
.bottom()
return view
}()
lazy var groupNameLab: UILabel = {
let label = UILabel()
label.text = " "
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = UIColor(hexStr: "#353B4F")
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: 90)
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)
return cv
}()
///
lazy var memberInfoView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 20
view.addSubview(memberNameLab)
memberNameLab.layoutChain
.top(20)
.left(18)
view.addSubview(tagView)
tagView.layoutChain
.leftToRightOfView(memberNameLab, offset: 6)
.centerY(memberNameLab)
.height(20)
ownView.layoutChain.height(16)
let statusView = UIView()
statusView.backgroundColor = .clear
statusView.addSubview(statusDotView)
statusView.addSubview(statusLab)
view.addSubview(statusView)
statusView.layoutChain
.right(18)
.centerY(memberNameLab)
statusDotView.layoutChain
.left()
.width(8)
.heightToWidth(1)
.centerY()
statusLab.layoutChain
.leftToRightOfView(statusDotView, offset: 4)
.edgesVertical()
.right()
let locationIcon = UIImageView()
locationIcon.image = UIImage(named: "Home/member_location")
view.addSubview(locationIcon)
locationIcon.layoutChain
.topToBottomOfView(memberNameLab, offset: 17)
.left(20)
.width(10)
.height(10)
view.addSubview(locationLab)
locationLab.layoutChain
.centerY(locationIcon)
.leftToRightOfView(locationIcon, offset: 7)
view.addSubview(distanceView)
distanceView.layoutChain
.right(18)
.centerY(locationLab)
locationLab.layoutChain.rightToLeftOfView(distanceView, offset: -30, relation: .greaterThanOrEqual)
return view
}()
lazy var memberNameLab: UILabel = {
let label = UILabel()
label.text = " "
label.font = .systemFont(ofSize: 12, weight: .bold)
label.textColor = UIColor(hexStr: "#353B4F")
return label
}()
lazy var tagView: UIStackView = {
let view = UIStackView(arrangedSubviews: [ownView])
view.axis = .horizontal
view.alignment = .center
view.spacing = 5
view.backgroundColor = .clear
return view
}()
//
lazy var ownView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#E3F6FF")
view.cornerRadius = 6
view.isHidden = true
let label = UILabel()
label.textColor = UIColor(hexStr: "#128CC6")
label.font = .systemFont(ofSize: 10, weight: .bold)
label.text = "圈主"
view.addSubview(label)
label.layoutChain
.edgesVertical(2)
.edgesHorzontal(8)
return view
}()
/// 线
lazy var statusDotView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#D8D8D8")
view.cornerRadius = 4
return view
}()
lazy var statusLab: UILabel = {
let label = UILabel()
label.text = " "
label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = UIColor(hexStr: "#353B4F")
return label
}()
//
lazy var locationLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#767676")
label.font = .systemFont(ofSize: 12, weight: .medium)
return label
}()
//
lazy var distanceView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#353B4F")
view.cornerRadius = 6
view.isHidden = true
let label = UILabel()
label.textColor = UIColor(hexStr: "#58EDFF")
label.font = .systemFont(ofSize: 10, weight: .bold)
label.text = "距离"
view.addSubview(label)
label.layoutChain
.left(5)
.edgesVertical(3)
view.addSubview(distanceLab)
distanceLab.layoutChain
.leftToRightOfView(label, offset: 2)
.right(5)
.centerY()
return view
}()
lazy var distanceLab: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 10, weight: .bold)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hexStr: "#FAFAFA")
cornerRadius = 30
layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,382 @@
//
// HomeView2.swift
// QuickLocation
//
// Created by on 2026/8/4.
//
import UIKit
import RxSwift
import RxCocoa
import AMapNaviKit
import MarqueeLabel
class HomeView2: UIView {
var disposeBag = DisposeBag()
let groupMemberView = GroupMemberView2(frame: .zero)
///
private let stage1Height: CGFloat = 160 + 49 + kSafeBottomMargin
private let stage2Height: CGFloat = 327 + 49 + kSafeBottomMargin
// MARK: - groupMemberView
private var groupMemberTopConstraint: NSLayoutConstraint?
///
private var stage3Top: CGFloat = 0
///
private var stage2Top: CGFloat = 0
///
private var stage1Top: CGFloat = 0
private var isGroupMemberLimitsSet = false
private var panStartTop: CGFloat = 0
/// scrollView
private var isSubCanScroll = false
private var groupMemberScrollTopOffset: CGFloat {
-groupMemberView.scrollView.adjustedContentInset.top
}
private var stageTops: [CGFloat] {
[stage3Top, stage2Top, stage1Top]
}
var gpsSignalViewList: [UIView] = []
///
func updateGPSSignal(bars: Int) {
let filled = max(0, min(bars, gpsSignalViewList.count))
for (i, view) in gpsSignalViewList.enumerated() {
view.backgroundColor = i < filled ? UIColor(hexStr: "#4ED178") : UIColor(hexStr: "#E0E0E0")
}
}
private func setupRx() {
//
warningView.rx.tapGesture.subscribe { _ in
LocationPermissionPopView.show()
}.disposed(by: disposeBag)
}
private func setupUI() {
addSubview(mapView)
addSubview(gpsSignalView)
addSubview(warningView)
addSubview(inviteBtn)
addSubview(locationView)
addSubview(groupMemberView)
mapView.layoutChain
.edges(excludingEdge: .bottom)
.bottom(49 + kSafeBottomMargin)
gpsSignalView.layoutChain
.top(kStatusBarHeight + 11)
.left(15)
warningView.layoutChain
.topToBottomOfView(gpsSignalView, offset: 11)
.leftToView(gpsSignalView)
groupMemberView.layoutChain
.top(kScreenHeight - stage2Height)
.edgesHorzontal()
.bottom()
inviteBtn.layoutChain
.bottomToTopOfView(groupMemberView, offset: -9)
.edgesHorzontal(15)
.heightToWidth(50/345)
locationView.layoutChain
.bottomToTopOfView(inviteBtn, offset: -8)
.left(15)
.width(40)
.height(40)
groupMemberTopConstraint = groupMemberView.jh_constraint(
.top, toAttribute: .top, otherView: self, relation: .equal
)
let pan = UIPanGestureRecognizer(target: self, action: #selector(handleGroupMemberPan(_:)))
pan.delegate = self
groupMemberView.addGestureRecognizer(pan)
groupMemberView.scrollView.delegate = self
groupMemberView.scrollView.isScrollEnabled = false
}
// MARK: - Map
lazy var mapView: MAMapView! = {
let mv = MAMapView()
mv.zoomLevel = 16
mv.showsUserLocation = true
mv.userTrackingMode = .none
mv.showsCompass = false
mv.showsScale = false
mv.isRotateEnabled = false
mv.isRotateCameraEnabled = false
DispatchQueue.main.async { mv.logoCenter = CGPoint(x: kScreenWidth - 50, y: kStatusBarHeight) }
return mv
}()
// MARK: -
lazy var gpsSignalView: UIView = {
let view = UIView()
view.cornerRadius = 6
view.backgroundColor = .white
let signalView1 = UIView()
signalView1.backgroundColor = UIColor(hexStr: "#E6E6E6")
view.addSubview(signalView1)
signalView1.layoutChain
.left(7)
.bottom(5)
.width(3)
.height(4)
let signalView2 = UIView()
signalView2.backgroundColor = UIColor(hexStr: "#E6E6E6")
view.addSubview(signalView2)
signalView2.layoutChain
.leftToRightOfView(signalView1, offset: 2)
.bottomToView(signalView1)
.width(3)
.height(6)
let signalView3 = UIView()
signalView3.backgroundColor = UIColor(hexStr: "#E6E6E6")
view.addSubview(signalView3)
signalView3.layoutChain
.leftToRightOfView(signalView2, offset: 2)
.bottomToView(signalView1)
.width(3)
.height(8)
let signalView4 = UIView()
signalView4.backgroundColor = UIColor(hexStr: "#E6E6E6")
view.addSubview(signalView4)
signalView4.layoutChain
.leftToRightOfView(signalView3, offset: 2)
.bottomToView(signalView1)
.width(3)
.height(10)
let label = UILabel()
label.text = "卫星信号"
label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = UIColor(hexStr: "#353B4F")
view.addSubview(label)
label.layoutChain
.leftToRightOfView(signalView4, offset: 2)
.edgesVertical(4)
.right(7)
gpsSignalViewList = [signalView1, signalView2, signalView3, signalView4]
return view
}()
// MARK: - warning
lazy var warningView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 8
view.isHidden = true
let icon = UIImageView()
let label = UILabel()
label.text = "有权限未开启,数据可能会异常"
label.textColor = UIColor(hexStr: "#353B4F")
label.font = .systemFont(ofSize: 12, weight: .medium)
view.addSubview(label)
label.layoutChain
.edgesHorzontal(8)
.edgesVertical(10)
return view
}()
// MARK: -
lazy var locationView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.4)
view.cornerRadius = 20
view.addSubview(locationIconView)
locationIconView.layoutChain.centerX().centerY()
return view
}()
lazy var locationIconView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.image = UIImage(named: "Home/location")
return view
}()
// MARK: -
lazy var inviteBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setBackgroundImage(UIImage(named: "Home/invite"), for: .normal)
return btn
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
if !isGroupMemberLimitsSet, bounds.height > 0 {
isGroupMemberLimitsSet = true
stage3Top = 0
stage2Top = bounds.height - stage2Height
stage1Top = bounds.height - stage1Height
groupMemberTopConstraint?.constant = stage2Top
}
}
// MARK: - Pan Gesture
@objc private func handleGroupMemberPan(_ pan: UIPanGestureRecognizer) {
guard isGroupMemberLimitsSet, let topConstraint = groupMemberTopConstraint else { return }
switch pan.state {
case .began:
layoutIfNeeded()
panStartTop = groupMemberView.frame.minY
case .changed:
let newTop = panStartTop + pan.translation(in: self).y
let scrollOffset = groupMemberView.scrollView.contentOffset.y
if isSubCanScroll {
if scrollOffset > groupMemberScrollTopOffset + 0.5 {
return
}
if pan.velocity(in: self).y > 0 || groupMemberView.frame.minY > stage3Top + 1 {
isSubCanScroll = false
panStartTop = groupMemberView.frame.minY
}
} else {
if groupMemberView.frame.minY <= stage3Top && newTop <= stage3Top {
isSubCanScroll = true
panStartTop = groupMemberView.frame.minY
topConstraint.constant = stage3Top
groupMemberView.setNavBarExpanded(true)
return
}
}
let clamped = max(stage3Top, min(stage1Top, newTop))
topConstraint.constant = clamped
case .ended, .cancelled:
let velocity = pan.velocity(in: self)
let currentTop = groupMemberView.frame.minY
let target = snapTarget(currentTop: currentTop, velocityY: velocity.y)
applyStage(top: target, animated: true)
default:
break
}
}
///
private func snapTarget(currentTop: CGFloat, velocityY: CGFloat) -> CGFloat {
if currentTop <= stage3Top + 5 {
// scroll
return nearestStage(to: currentTop)
}
if abs(velocityY) > 200 {
if velocityY < 0 {
// top
return stageTops.filter { $0 < currentTop - 1 }.max() ?? stage3Top
} else {
// top
return stageTops.filter { $0 > currentTop + 1 }.min() ?? stage1Top
}
}
return nearestStage(to: currentTop)
}
private func nearestStage(to top: CGFloat) -> CGFloat {
stageTops.min(by: { abs($0 - top) < abs($1 - top) }) ?? stage2Top
}
private func applyStage(top: CGFloat, animated: Bool) {
guard let topConstraint = groupMemberTopConstraint else { return }
let atTop = abs(top - stage3Top) < 0.5
topConstraint.constant = top
if !atTop {
isSubCanScroll = false
groupMemberView.scrollView.isScrollEnabled = false
groupMemberView.scrollView.setContentOffset(CGPoint(x: 0, y: groupMemberScrollTopOffset), animated: false)
}
isSubCanScroll = atTop
groupMemberView.setNavBarExpanded(atTop)
let animations = {
self.layoutIfNeeded()
}
let completion: (Bool) -> Void = { _ in
self.groupMemberView.scrollView.isScrollEnabled = atTop
}
if animated {
UIView.animate(withDuration: 0.25, delay: 0,
options: [.curveEaseOut, .allowUserInteraction],
animations: animations, completion: completion)
} else {
animations()
completion(true)
}
}
/// GroupMemberView2
func dismissGroupMemberView() {
guard isGroupMemberLimitsSet else { return }
applyStage(top: stage2Top, animated: true)
}
}
// MARK: - UIScrollViewDelegate
extension HomeView2: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
guard scrollView == groupMemberView.scrollView else { return }
if scrollView.contentOffset.y > groupMemberScrollTopOffset {
isSubCanScroll = true
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == groupMemberView.scrollView else { return }
if isSubCanScroll {
if scrollView.contentOffset.y <= groupMemberScrollTopOffset {
isSubCanScroll = false
scrollView.contentOffset = CGPoint(x: 0, y: groupMemberScrollTopOffset)
}
} else {
scrollView.contentOffset = CGPoint(x: 0, y: groupMemberScrollTopOffset)
}
}
}
// MARK: - UIGestureRecognizerDelegate
extension HomeView2: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
return true
}
}

View File

@ -26,13 +26,15 @@ class HomeViewController: BaseViewController {
override var preferredStatusBarStyle: UIStatusBarStyle { .default }
// MARK: - Properties
fileprivate var rootView: HomeView!
fileprivate var rootView: HomeView2!
private var tableView: UITableView {
rootView.groupMemberView.tableView
private var memberCV: UICollectionView {
rootView.groupMemberView.memberCV
}
private var viewModel = HomeViewModel()
/// GroupMemberListCell
private var selectedMemberId: String = ""
private let locationManager = CLLocationManager()
private var currentHeading: Double = 0
@ -63,7 +65,7 @@ class HomeViewController: BaseViewController {
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
#endif
rootView = HomeView(frame: UIScreen.main.bounds)
rootView = HomeView2(frame: UIScreen.main.bounds)
view = rootView
}
@ -74,7 +76,7 @@ class HomeViewController: BaseViewController {
setupHeading()
reactiveAction()
rootView.quickMessageView.tagListView.delegate = self
// rootView.quickMessageView.tagListView.delegate = self
// MQTT
UIDevice.current.isBatteryMonitoringEnabled = true
startLocationTimer()
@ -84,7 +86,7 @@ class HomeViewController: BaseViewController {
super.viewDidAppear(animated)
let manager = AuthorizeManager.manager(type: .locationAlways)
rootView.warningIconView.isHidden = manager?.authorizeStatus() == .authorized
rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized
}
override func viewDidDisappear(_ animated: Bool) {
@ -140,49 +142,49 @@ class HomeViewController: BaseViewController {
// MARK: - Actions
private func reactiveAction() {
//
rootView.bubbleView.rx.tapGesture.subscribe { _ in
AppRouter.push(Route.createBubble)
}.disposed(by: disposeBag)
// rootView.bubbleView.rx.tapGesture.subscribe { _ in
// AppRouter.push(Route.createBubble)
// }.disposed(by: disposeBag)
//
rootView.signInView.rx.tapGesture.subscribe { _ in
let vc = SignInVC(lastLocation: self.lastLocation)
vc.isNeedLogin = true
AppRouter.push(vc)
}.disposed(by: disposeBag)
// rootView.signInView.rx.tapGesture.subscribe { _ in
// let vc = SignInVC(lastLocation: self.lastLocation)
// vc.isNeedLogin = true
// AppRouter.push(vc)
// }.disposed(by: disposeBag)
// SOS
rootView.sosView.rx.tapGesture.subscribe { _ in
let vc = SOSViewController()
vc.isNeedLogin = true
AppRouter.push(vc)
}.disposed(by: disposeBag)
// rootView.sosView.rx.tapGesture.subscribe { _ in
// let vc = SOSViewController()
// vc.isNeedLogin = true
// AppRouter.push(vc)
// }.disposed(by: disposeBag)
//
rootView.scheduleView.rx.tapGesture.subscribe { _ in
guard let model = self.viewModel.groupModel else { return }
AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key])
}.disposed(by: disposeBag)
// rootView.scheduleView.rx.tapGesture.subscribe { _ in
// guard let model = self.viewModel.groupModel else { return }
// AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key])
// }.disposed(by: disposeBag)
//
rootView.groupView.rx.tapGesture.subscribe { _ in
guard let groupModel = self.viewModel.groupModel else { return }
let groupViewFrame = self.view.convert(self.rootView.groupView.frame, from: self.rootView)
let startPointY = groupViewFrame.origin.y + groupViewFrame.height
GroupListPopView.show(start: CGPoint(x: 0, y: startPointY + 20),
groupModel: groupModel) { groupKey in
guard let key = groupKey else { return }
self.requestOperateGroup(groupKey: key)
}
}.disposed(by: disposeBag)
// rootView.groupView.rx.tapGesture.subscribe { _ in
// guard let groupModel = self.viewModel.groupModel else { return }
// let groupViewFrame = self.view.convert(self.rootView.groupView.frame, from: self.rootView)
// let startPointY = groupViewFrame.origin.y + groupViewFrame.height
// GroupListPopView.show(start: CGPoint(x: 0, y: startPointY + 20),
// groupModel: groupModel) { groupKey in
// guard let key = groupKey else { return }
// self.requestOperateGroup(groupKey: key)
// }
// }.disposed(by: disposeBag)
//
rootView.groupMemberView.refreshBtn.rx.tap.subscribe(onNext: { _ in
self.requestGroupInfo()
}).disposed(by: disposeBag)
// rootView.groupMemberView.refreshBtn.rx.tap.subscribe(onNext: { _ in
// self.requestGroupInfo()
// }).disposed(by: disposeBag)
//
rootView.groupMemberView.inviteJoinBtn.rx.tap.subscribe(onNext: { _ in
//
rootView.inviteBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.viewModel.groupInfo])
}).disposed(by: disposeBag)
@ -206,7 +208,7 @@ class HomeViewController: BaseViewController {
guard let self = self, let model = response.model else { return }
Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config
rootView.searchLottieView.isHidden = model.config?.isIntercept == true
// rootView.searchLottieView.isHidden = model.config?.isIntercept == true
self.getUserIMToken()
self.requestUserInfo { [weak self] in
self?.requestGroupInfo()
@ -249,66 +251,85 @@ class HomeViewController: BaseViewController {
}.disposed(by: disposeBag)
//
rootView.onDismissPanel = { [weak self] in
self?.isMemberPanelShown = false
(self?.tabBarController as? MainTabBarController)?.setTabBarHidden(false)
}
// rootView.onDismissPanel = { [weak self] in
// self?.isMemberPanelShown = false
// (self?.tabBarController as? MainTabBarController)?.setTabBarHidden(false)
// }
//
rootView.interactionView.onNavigate = { [weak self] in
guard let self = self, let member = self.rootView.interactionView.currentMember else { return }
self.rootView.dismissMemberPanel()
let userCoord = self.lastLocation?.coordinate
let groupName = self.viewModel.groupName
let model = self.viewModel.groupModel
let iconIndex = model?.groups.first(where: { $0.group_key == model?.default_group_key })?.icon_index ?? 1
let vc = NavigationVC(member: member, currentUserCoord: userCoord, groupName: groupName, groupIcon: "\(iconIndex)")
self.navigationController?.pushViewController(vc, animated: true)
}
// rootView.interactionView.onNavigate = { [weak self] in
// guard let self = self, let member = self.rootView.interactionView.currentMember else { return }
// self.rootView.dismissMemberPanel()
// let userCoord = self.lastLocation?.coordinate
// let groupName = self.viewModel.groupName
// let model = self.viewModel.groupModel
// let iconIndex = model?.groups.first(where: { $0.group_key == model?.default_group_key })?.icon_index ?? 1
// let vc = NavigationVC(member: member, currentUserCoord: userCoord, groupName: groupName, groupIcon: "\(iconIndex)")
// self.navigationController?.pushViewController(vc, animated: true)
// }
//
rootView.interactionView.onSendEmote = { [weak self] emoteIdx in
guard let self = self else { return }
self.requestSendEmote(emoteIdx: emoteIdx)
}
// rootView.interactionView.onSendEmote = { [weak self] emoteIdx in
// guard let self = self else { return }
// self.requestSendEmote(emoteIdx: emoteIdx)
// }
//
rootView.interactionView.onShare = {
SharePopView.show()
}
// rootView.interactionView.onShare = {
// SharePopView.show()
// }
}
private func bindViewModel() {
viewModel.output.sectionedItems
.bind(to: tableView.rx.items(dataSource: dataSource))
.do(onNext: { [weak self] sections in
guard let self = self else { return }
let items = sections.flatMap(\.items)
guard !items.isEmpty else { return }
let stillValid = items.contains { $0.user_id == self.selectedMemberId }
if self.selectedMemberId.isEmpty || !stillValid {
if let me = items.first(where: { self.viewModel.isCurrentUser(id: $0.user_id) }) {
self.selectedMemberId = me.user_id
self.rootView.groupMemberView.setupMemberInfo(
me,
isOwner: self.viewModel.isGroupOwn(id: me.user_id)
)
} else {
self.selectedMemberId = items[0].user_id
self.rootView.groupMemberView.setupMemberInfo(
items[0],
isOwner: self.viewModel.isGroupOwn(id: items[0].user_id)
)
}
}
})
.bind(to: memberCV.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
let selectedMember = tableView.rx.modelSelected(GroupMemberModel.self)
let selectedMember = memberCV.rx.modelSelected(GroupMemberModel.self)
.share()
selectedMember
.subscribe(viewModel.cellAction.inputs)
.disposed(by: disposeBag)
//
// +
selectedMember
.subscribe(onNext: { [weak self] model in
guard let self = self else { return }
self.locateMember(userId: model.user_id)
self?.selectMember(userId: model.user_id, centerMap: true, dismissPanel: true)
})
.disposed(by: disposeBag)
}
// MARK: - UITableViewDataSource
lazy private var dataSource: RxTableViewSectionedReloadDataSource<GroupMemberListSectionModel> = {
return RxTableViewSectionedReloadDataSource<GroupMemberListSectionModel>(
configureCell: { (_, tableView, indexPath, model) in
let cell: GroupMemberCell = tableView.dequeueReusableCell(for: indexPath)
// MARK: - UICollectionViewDataSource
lazy private var dataSource: RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> { [weak self] _, collectionView, indexPath, model in
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(model: model,
isCurrentUser: self.viewModel.isCurrentUser(id: model.user_id),
isOwn: self.viewModel.isGroupOwn(id: model.user_id))
isCurrentUser: self?.viewModel.isCurrentUser(id: model.user_id) ?? false,
isSelected: model.user_id == self?.selectedMemberId)
return cell
})
}
}()
// MARK: - API
@ -329,7 +350,7 @@ class HomeViewController: BaseViewController {
UserService.userInfo().subscribe { response in
guard let model = response.model else { return }
AppContextManager.shared.saveAccount(model)
self.rootView.avatarImgView.image = model.userIcon
// self.rootView.avatarImgView.image = model.userIcon
completion?()
}.disposed(by: disposeBag)
}
@ -341,8 +362,8 @@ class HomeViewController: BaseViewController {
NotificationCenter.default.post(name: .RefreshIMGroupListNotification, object: nil)
guard isDefaultGroup else { return }
self.rootView.groupMemberView.setupCountData(self.viewModel.memberCount, 1)
self.rootView.groupNameLab.text = self.viewModel.groupName
// self.rootView.groupMemberView.setupCountData(self.viewModel.memberCount, 1)
self.rootView.groupMemberView.groupNameLab.text = self.viewModel.groupName
self.syncMemberAnnotations(model.select_group_employee)
self.refreshMQTTSubscriptions(model.select_group_employee)
}.disposed(by: disposeBag)
@ -355,19 +376,19 @@ class HomeViewController: BaseViewController {
}
private func requestNotice() {
UserService.notice().subscribe { response in
self.rootView.noticeLab.text = ""
guard let data = response.data, let noticeList = data["notice"] as? [String] else { return }
self.rootView.noticeView.isHidden = noticeList.count == 0
self.rootView.noticeLab.text = noticeList.joined(separator: " ") + " "
if let loop = data["loop"] as? Bool {
self.rootView.noticeLab.type = loop ? .continuous : .leftRight
self.rootView.noticeLab.onScrollLoopComplete = {
guard loop == false else { return }
self.rootView.noticeView.isHidden = true
}
}
}.disposed(by: disposeBag)
// UserService.notice().subscribe { response in
// self.rootView.noticeLab.text = ""
// guard let data = response.data, let noticeList = data["notice"] as? [String] else { return }
// self.rootView.noticeView.isHidden = noticeList.count == 0
// self.rootView.noticeLab.text = noticeList.joined(separator: " ") + " "
// if let loop = data["loop"] as? Bool {
// self.rootView.noticeLab.type = loop ? .continuous : .leftRight
// self.rootView.noticeLab.onScrollLoopComplete = {
// guard loop == false else { return }
// self.rootView.noticeView.isHidden = true
// }
// }
// }.disposed(by: disposeBag)
}
// MARK: - Map Setup
@ -404,7 +425,7 @@ class HomeViewController: BaseViewController {
if hitRect.contains(point) {
self.isMemberPanelShown = true
(self.tabBarController as? MainTabBarController)?.setTabBarHidden(true)
rootView.showMemberPanel(member: ann.member)
// rootView.showMemberPanel(member: ann.member)
viewModel.targetUid = ann.member.id
break
}
@ -511,14 +532,58 @@ class HomeViewController: BaseViewController {
#endif
}
/// /
private func selectMember(userId: String, centerMap: Bool = true, dismissPanel: Bool = false) {
let listUserId = (userId == "current") ? AppContextManager.shared.userId : userId
guard !listUserId.isEmpty else { return }
selectedMemberId = listUserId
if let model = viewModel.memberList.first(where: { $0.user_id == listUserId }) {
rootView.groupMemberView.setupMemberInfo(
model,
isOwner: viewModel.isGroupOwn(id: model.user_id)
)
}
memberCV.reloadData()
if let idx = viewModel.memberList.firstIndex(where: { $0.user_id == listUserId }) {
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: true)
}
}
#if !targetEnvironment(simulator)
if dismissPanel {
rootView.dismissGroupMemberView()
}
if centerMap {
centerMapOnMember(userId: userId)
}
#endif
}
#if !targetEnvironment(simulator)
/// id "current" userId
private func centerMapOnMember(userId: String) {
let coordinate: CLLocationCoordinate2D?
if userId == "current" || viewModel.isCurrentUser(id: userId) {
coordinate = currentUserAnnotation?.coordinate
?? lastLocation?.coordinate
} else {
coordinate = members.first(where: { $0.id == userId })?.coordinate
}
guard let coord = coordinate, CLLocationCoordinate2DIsValid(coord) else { return }
rootView.mapView.setCenter(coord, animated: true)
rootView.mapView.setZoomLevel(16, animated: true)
}
/// GroupMemberView
private func locateMember(userId: String) {
rootView.dismissGroupMemberView()
guard let member = members.first(where: { $0.id == userId }),
CLLocationCoordinate2DIsValid(member.coordinate) else { return }
rootView.mapView.setCenter(member.coordinate, animated: true)
rootView.mapView.setZoomLevel(16, animated: true)
selectMember(userId: userId, centerMap: true, dismissPanel: true)
}
#endif
@ -623,28 +688,28 @@ extension HomeViewController {
let emoteUserId = parts[1]
if firstChar == "1" || firstChar == "2" { //
let emojiFileName = firstChar == "1" ? "normal_" : "fun_"
// let emojiFileName = firstChar == "1" ? "normal_" : "fun_"
let nickName = self.viewModel.getUserNickName(id: emoteUserId)
let fullText = nickName + "对你发送了表情"
let attr = NSMutableAttributedString(string: fullText)
attr.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .medium), range: NSRange(location: 0, length: fullText.count))
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: NSRange(location: 0, length: nickName.count))
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.rootView.messageLab.attributedText = attr
self.rootView.messageBubbleView.isHidden = false
if let path = Bundle.main.path(forResource: "\(emojiFileName)\(lastChar)", ofType: "json") {
self.rootView.emojiPopView.animation = LottieAnimation.filepath(path)
self.rootView.bringSubviewToFront(self.rootView.emojiPopView)
self.rootView.emojiPopView.isHidden = false
self.rootView.emojiPopView.play { completed in
self.rootView.emojiPopView.isHidden = true
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.rootView.messageBubbleView.isHidden = true
}
}
// DispatchQueue.main.async { [weak self] in
// guard let self = self else { return }
// self.rootView.messageLab.attributedText = attr
// self.rootView.messageBubbleView.isHidden = false
// if let path = Bundle.main.path(forResource: "\(emojiFileName)\(lastChar)", ofType: "json") {
// self.rootView.emojiPopView.animation = LottieAnimation.filepath(path)
// self.rootView.bringSubviewToFront(self.rootView.emojiPopView)
// self.rootView.emojiPopView.isHidden = false
// self.rootView.emojiPopView.play { completed in
// self.rootView.emojiPopView.isHidden = true
// }
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// self.rootView.messageBubbleView.isHidden = true
// }
// }
}
else if firstChar == "3" { //
let textIdx = lastChar.integer
@ -656,14 +721,14 @@ extension HomeViewController {
attr.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .medium), range: NSRange(location: 0, length: fullText.count))
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: NSRange(location: 0, length: nickName.count))
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#333333"), range: NSRange(location: nickName.count, length: texts[textIdx].count))
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.rootView.messageLab.attributedText = attr
self.rootView.messageBubbleView.isHidden = false
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.rootView.messageBubbleView.isHidden = true
}
}
// DispatchQueue.main.async { [weak self] in
// guard let self = self else { return }
// self.rootView.messageLab.attributedText = attr
// self.rootView.messageBubbleView.isHidden = false
// DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// self.rootView.messageBubbleView.isHidden = true
// }
// }
}
case "bubble": //
guard let userId = msg.data?.user_id else { return }
@ -683,18 +748,18 @@ extension HomeViewController {
guard !self.isSOSPlaying else { return }
self.isSOSPlaying = true
self.playSOSAlarm()
self.rootView.sosPopView.isHidden = false
self.rootView.sosPopView.play { completed in
self.rootView.sosPopView.stop()
self.rootView.sosPopView.isHidden = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak self] in
self?.sosPlayer?.stop()
self?.sosPlayer = nil
self?.rootView.sosPopView.stop()
self?.rootView.sosPopView.isHidden = true
self?.isSOSPlaying = false
}
// self.rootView.sosPopView.isHidden = false
// self.rootView.sosPopView.play { completed in
// self.rootView.sosPopView.stop()
// self.rootView.sosPopView.isHidden = true
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak self] in
// self?.sosPlayer?.stop()
// self?.sosPlayer = nil
// self?.rootView.sosPopView.stop()
// self?.rootView.sosPopView.isHidden = true
// self?.isSOSPlaying = false
// }
}
case "join": //
guard let gk = msg.data?.group_key,
@ -726,7 +791,7 @@ extension HomeViewController {
guard self.viewModel.memberIsExist(userId: userId) else { return }
self.viewModel.removeMember(userId: userId)
self.removeAnnotation(userId: userId)
self.rootView.groupMemberView.setupCountData(self.viewModel.memberList.count, 1)
// self.rootView.groupMemberView.setupCountData(self.viewModel.memberList.count, 1)
MQTTService.shared.unsubscribe(topic: "smartdrive/\(userId)")
}
case "dismiss": //
@ -754,7 +819,7 @@ extension HomeViewController {
private func updateOnlineCount() {
let total = viewModel.memberCount
let online = viewModel.memberList.filter { $0.is_online }.count
rootView.groupMemberView.setupCountData(total, online)
// rootView.groupMemberView.setupCountData(total, online)
}
///
@ -818,8 +883,9 @@ extension HomeViewController: MAMapViewDelegate {
}
annotationView?.configure(with: memberAnnotation.member)
annotationView?.onTap = { [weak self] ann in
self?.selectMember(userId: ann.member.id, centerMap: true, dismissPanel: false)
}
if memberAnnotation.member.isCurrentUser {
currentUserAnnotation = memberAnnotation

View File

@ -41,7 +41,7 @@ class LoginView: UIView {
}
func setupAgreementTextWithCarrier(carrier: String) {
let text = carrier.isEmpty ? "登录即同意《隐私政策》和《用户协议》" : "登录即同意\(carrier)、《隐私政策》和《用户协议》"
let text = carrier.isEmpty ? "登录即同意《用户协议》和《隐私政策" : "登录即同意\(carrier)、《隐私政策》和《用户协议》"
agreementLabel.text = text
//
let paragraphStyle = NSMutableParagraphStyle()
@ -65,27 +65,31 @@ class LoginView: UIView {
}
private func setupUI() {
addSubview(bgMaskImage)
// addSubview(bgMaskImage)
addSubview(backBtn)
addSubview(guestLoginButton)
addSubview(welcomeView)
welcomeView.addSubview(welcomeLineView)
welcomeView.addSubview(welcomeLabel)
welcomeView.addSubview(welcomeTitleLab)
addSubview(inputContainerView)
inputContainerView.addSubview(welcomeLabel)
inputContainerView.addSubview(phoneNumberLabel)
inputContainerView.addSubview(carrierLabel)
inputContainerView.addSubview(otherPhoneView)
otherPhoneView.addSubview(phoneInputView)
otherPhoneView.addSubview(smsCodeInputView)
inputContainerView.addSubview(loginButton)
inputContainerView.addSubview(phoneInputView)
inputContainerView.addSubview(smsCodeInputView)
addSubview(loginButton)
addSubview(appleLoginBtn)
addSubview(wechatLoginBtn)
addSubview(phoneLoginBtn)
addSubview(otherLoginTitleLab)
addSubview(leftSeparator)
addSubview(rightSeparator)
addSubview(agreementView)
agreementView.addSubview(checkBox)
agreementView.addSubview(agreementTV)
agreementView.addSubview(agreementLabel)
bgMaskImage.layoutChain.edges()
// bgMaskImage.layoutChain.edges()
backBtn.layoutChain
.top(54)
@ -100,31 +104,30 @@ class LoginView: UIView {
.height(29)
inputContainerView.layoutChain
.centerX()
.centerY(self, offset: -17)
.centerY()
.edgesHorzontal()
.height(306)
welcomeView.layoutChain
.bottomToTopOfView(inputContainerView, offset: -100)
.left(38)
welcomeLineView.layoutChain
.edges(excludingEdge: .right)
.width(4)
welcomeLabel.layoutChain
.top(0)
.centerX()
.edgesHorzontal()
.top()
.leftToRightOfView(welcomeLineView, offset: 12)
phoneNumberLabel.layoutChain
.topToBottomOfView(welcomeLabel, offset: 78)
.centerX()
carrierLabel.layoutChain
.topToBottomOfView(phoneNumberLabel, offset: 4)
.centerX()
otherPhoneView.layoutChain
.topToBottomOfView(welcomeLabel, offset: 43)
.centerX()
welcomeTitleLab.layoutChain
.topToBottomOfView(welcomeLabel)
.leftToView(welcomeLabel)
.right()
.bottom()
phoneInputView.layoutChain
.edges(excludingEdge: .bottom)
.width(259)
.edgesHorzontal(38)
.height(50)
phoneLineView.layoutChain
@ -145,13 +148,14 @@ class LoginView: UIView {
smsCodeInputView.layoutChain
.topToBottomOfView(phoneInputView, offset: 20)
.edges(excludingEdge: .top)
.widthToView(phoneInputView)
.leftToView(phoneInputView)
.rightToView(phoneInputView)
.heightToView(phoneInputView)
.bottom()
smsCodeBtn.layoutChain
.edgesVertical()
.right()
.edgesVertical(5)
.right(5)
.width(100)
smsCodeTF.layoutChain
@ -160,15 +164,13 @@ class LoginView: UIView {
.rightToLeftOfView(smsCodeBtn)
loginButton.layoutChain
.topToBottomOfView(otherPhoneView, offset: 63)
.centerX()
.width(247)
.height(50)
.topToBottomOfView(inputContainerView, offset: 46)
.edgesHorzontal(30)
.height(56)
agreementView.layoutChain
.leftToView(loginButton)
.rightToView(loginButton)
.bottom(kSafeBottomMargin + 20)
.topToBottomOfView(loginButton, offset: 14)
.centerX()
.height(30)
agreementLabel.layoutChain.edges()
@ -185,9 +187,9 @@ class LoginView: UIView {
.top()
phoneLoginBtn.layoutChain
.bottomToTopOfView(agreementView, offset: -30)
.bottom(kSafeBottomMargin + 30)
.centerX()
.width(38)
.width(40)
.heightToWidth(1)
appleLoginBtn.layoutChain
@ -201,6 +203,22 @@ class LoginView: UIView {
.centerY(phoneLoginBtn)
.widthToView(phoneLoginBtn)
.heightToView(phoneLoginBtn)
otherLoginTitleLab.layoutChain
.bottomToTopOfView(phoneLoginBtn, offset: -18)
.centerX()
leftSeparator.layoutChain
.centerY(otherLoginTitleLab)
.rightToLeftOfView(otherLoginTitleLab, offset: -14)
.width(60)
.height(2)
rightSeparator.layoutChain
.centerY(otherLoginTitleLab)
.leftToRightOfView(otherLoginTitleLab, offset: 14)
.widthToView(leftSeparator)
.heightToView(leftSeparator)
}
override init(frame: CGRect) {
@ -215,6 +233,24 @@ class LoginView: UIView {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
leftSeparator.layoutIfNeeded()
leftSeparator.setGradientLayer(frame: leftSeparator.bounds,
startPoint: CGPoint(x: 0, y: 0.5),
endPoint: CGPoint(x: 1, y: 0.5),
colors: [UIColor(hexStr: "#D8D8D8", alpha: 0), UIColor(hexStr: "#E5E5E5")],
locations: [0, 1])
rightSeparator.layoutIfNeeded()
rightSeparator.setGradientLayer(frame: rightSeparator.bounds,
startPoint: CGPoint(x: 0, y: 0.5),
endPoint: CGPoint(x: 1, y: 0.5),
colors: [UIColor(hexStr: "#E5E5E5"), UIColor(hexStr: "#D8D8D8", alpha: 0)],
locations: [0, 1])
}
// MARK: - UI Components
lazy var bgMaskImage: UIImageView = {
let iv = UIImageView()
@ -240,12 +276,32 @@ class LoginView: UIView {
return btn
}()
lazy var welcomeView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var welcomeLineView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#99F2FC")
view.cornerRadius = 2
return view
}()
lazy var welcomeLabel: UILabel = {
let label = UILabel()
label.text = "欢迎登录"
label.font = UIFont(name: "DOUYU Font", size: 24) ?? .systemFont(ofSize: 24, weight: .medium)
label.textColor = UIColor(hexStr: "#030303")
label.textAlignment = .center
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = .black
return label
}()
lazy var welcomeTitleLab: UILabel = {
let label = UILabel()
label.text = "极速定位"
label.font = .systemFont(ofSize: 32, weight: .bold)
label.textColor = .black
return label
}()
@ -272,21 +328,11 @@ class LoginView: UIView {
return view
}()
//
lazy var otherPhoneView: UIView = {
let view = UIView()
view.backgroundColor = .clear
// view.isHidden = true
return view
}()
//
lazy var phoneInputView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.borderWidth = 1
view.borderColor = .white
view.cornerRadius = 8
view.backgroundColor = UIColor(hexStr: "#F8F8F8")
view.cornerRadius = 12
view.addSubview(areaCodeLab)
view.addSubview(phoneLineView)
view.addSubview(phoneInputTF)
@ -297,14 +343,14 @@ class LoginView: UIView {
let label = UILabel()
label.text = "+86"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .white
label.textColor = UIColor(hexStr: "#3D3D3D")
label.textAlignment = .center
return label
}()
lazy var phoneLineView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.backgroundColor = UIColor(hexStr: "#D8D8D8")
return view
}()
@ -312,7 +358,7 @@ class LoginView: UIView {
let textField = UITextField()
textField.font = .systemFont(ofSize: 16, weight: .medium)
textField.placeholderColor(placeholder: "请输入手机号码",
color: .white)
color: UIColor(hexStr: "#767676"))
// textField.tintColor = ThemeManager.shared.color.mainColor
textField.clearButtonMode = .whileEditing
textField.keyboardType = .numberPad
@ -322,10 +368,8 @@ class LoginView: UIView {
//
lazy var smsCodeInputView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.borderWidth = 1
view.borderColor = .white
view.cornerRadius = 8
view.backgroundColor = UIColor(hexStr: "#F8F8F8")
view.cornerRadius = 12
view.addSubview(smsCodeTF)
view.addSubview(smsCodeBtn)
return view
@ -335,29 +379,53 @@ class LoginView: UIView {
let textField = UITextField()
textField.font = .systemFont(ofSize: 16, weight: .medium)
textField.placeholderColor(placeholder: "请输入验证码",
color: .white)
color: UIColor(hexStr: "#767676"))
textField.keyboardType = .numberPad
return textField
}()
lazy var smsCodeBtn: UIButton = {
let button = UIButton()
button.backgroundColor = .white
button.cornerRadius = 8
button.setTitle("获取验证码", for: .normal)
button.setTitleColor(UIColor(hexStr: "#030303"), for: .normal)
button.setTitleColor(UIColor(hexStr: "#999999", alpha: 1.0), for: .disabled)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
button.setTitleColor(UIColor(hexStr: "#1A1A1A"), for: .normal)
button.setTitleColor(UIColor(hexStr: "#767676"), for: .disabled)
button.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
return button
}()
lazy var loginButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("立即登录", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Login/login_bg"), for: .normal)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 20
return btn
}()
lazy var otherLoginTitleLab: UILabel = {
let label = UILabel()
label.text = "其他登录方式"
label.font = .systemFont(ofSize: 14, weight: .regular)
label.textColor = UIColor(hexStr: "#767676")
return label
}()
lazy var leftSeparator: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var rightSeparator: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var wechatLoginBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setBackgroundImage(UIImage(named: "Login/wechat"), for: .normal)

View File

@ -57,10 +57,10 @@ class OneTapLoginView: UIView {
}
private func setupUI() {
addSubview(bgMaskImage)
// addSubview(bgMaskImage)
addSubview(backBtn)
addSubview(inputContainerView)
inputContainerView.addSubview(welcomeLabel)
// inputContainerView.addSubview(welcomeLabel)
inputContainerView.addSubview(phoneNumberLabel)
inputContainerView.addSubview(carrierLabel)
inputContainerView.addSubview(loginButton)
@ -70,7 +70,7 @@ class OneTapLoginView: UIView {
agreementView.addSubview(agreementLabel)
agreementView.addSubview(agreementTV)
bgMaskImage.layoutChain.edges()
// bgMaskImage.layoutChain.edges()
backBtn.layoutChain
.top(54)
@ -84,28 +84,27 @@ class OneTapLoginView: UIView {
.edgesHorzontal()
.height(301)
welcomeLabel.layoutChain
.top(0)
.centerX()
.edgesHorzontal()
// welcomeLabel.layoutChain
// .top(0)
// .centerX()
// .edgesHorzontal()
phoneNumberLabel.layoutChain
.topToBottomOfView(welcomeLabel, offset: 78)
.top()
.centerX()
carrierLabel.layoutChain
.topToBottomOfView(phoneNumberLabel, offset: 4)
.topToBottomOfView(phoneNumberLabel, offset: 14)
.centerX()
loginButton.layoutChain
.topToBottomOfView(carrierLabel, offset: 99)
.centerX()
.width(247)
.height(50)
.edgesHorzontal(30)
.height(56)
agreementView.layoutChain
.topToBottomOfView(inputContainerView, offset: 14)
.edgesHorzontal(30)
.bottom(kSafeBottomMargin + 20)
checkBox.layoutChain
.left()
@ -166,7 +165,7 @@ class OneTapLoginView: UIView {
lazy var welcomeLabel: UILabel = {
let label = UILabel()
label.text = "欢迎登录"
// label.text = ""
label.font = UIFont(name: "DOUYU Font", size: 24) ?? .systemFont(ofSize: 24, weight: .medium)
label.textColor = UIColor(hexStr: "#030303")
label.textAlignment = .center
@ -175,16 +174,16 @@ class OneTapLoginView: UIView {
lazy var phoneNumberLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 20, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 32, weight: .heavy)
label.textColor = UIColor(hexStr: "#3D3D3D")
label.textAlignment = .center
return label
}()
lazy var carrierLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = ThemeManager.shared.color.contentColor
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#767676")
label.textAlignment = .center
return label
}()
@ -192,9 +191,10 @@ class OneTapLoginView: UIView {
lazy var loginButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("本机号码一键登录", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Login/login_bg"), for: .normal)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 20
return btn
}()

View File

@ -13,53 +13,51 @@ final class MemberAnnotationView: MAAnnotationView {
var onTap: ((MemberAnnotation) -> Void)?
// MARK: - Design Constants
static let avatarOuterSize: CGFloat = 50
static let avatarInnerSize: CGFloat = 48
static let nameTagWidth: CGFloat = 60
static let nameTagHeight: CGFloat = 20
static let totalHeight: CGFloat = avatarOuterSize + 4 + nameTagHeight
static let radarSize: CGFloat = 180
private static let bubbleSize: CGFloat = 56
private static let bubbleCorner: CGFloat = 14
private static let borderWidth: CGFloat = 4
private static let pointerWidth: CGFloat = 12
private static let pointerHeight: CGFloat = 7
private static let gapBubbleToDot: CGFloat = 4
private static let gapNameToBubble: CGFloat = 4
private static let nameTagHeight: CGFloat = 20
private static let nameTagHPadding: CGFloat = 8
private static let dotOuterSize: CGFloat = 12
private static let dotInnerSize: CGFloat = 10
private let containerView: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
private static let currentUserDotColor = UIColor(hexStr: "#5ED4FF")
private static let memberDotPalette: [UIColor] = [
UIColor(hexStr: "#FF6B6B"),
UIColor(hexStr: "#FF9F43"),
UIColor(hexStr: "#FECA57"),
UIColor(hexStr: "#1DD1A1"),
UIColor(hexStr: "#54A0FF"),
UIColor(hexStr: "#5F27CD"),
UIColor(hexStr: "#FF9FF3"),
UIColor(hexStr: "#00D2D3"),
UIColor(hexStr: "#EE5A24"),
UIColor(hexStr: "#10AC84"),
UIColor(hexStr: "#2E86DE"),
UIColor(hexStr: "#341F97"),
]
private let avatarOuterCircle: UIView = {
private var showsNameTag = false
private let containerView = UIView()
private let nameTagView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = avatarOuterSize / 2
v.layer.shadowColor = UIColor.black.withAlphaComponent(0.15).cgColor
v.layer.shadowOffset = CGSize(width: 0, height: 2)
v.layer.shadowRadius = 4
v.layer.shadowOpacity = 1
return v
}()
private let avatarImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.layer.cornerRadius = avatarInnerSize / 2
iv.layer.borderWidth = 1
iv.layer.borderColor = UIColor.white.cgColor
iv.clipsToBounds = true
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
return iv
}()
private lazy var nameTagView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = 4
v.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor
v.layer.cornerRadius = 8
v.layer.shadowColor = UIColor.black.cgColor
v.layer.shadowOffset = CGSize(width: 0, height: 1)
v.layer.shadowRadius = 2
v.layer.shadowOpacity = 1
v.layer.shadowOpacity = 0.12
v.isHidden = true
return v
}()
private lazy var nameLabel: UILabel = {
private let nameLabel: UILabel = {
let l = UILabel()
l.font = UIFont(name: "PingFangSC-Medium", size: 11)
?? UIFont.systemFont(ofSize: 11, weight: .medium)
@ -69,10 +67,55 @@ final class MemberAnnotationView: MAAnnotationView {
return l
}()
// MARK: - Layers
private var headingLayer: CAShapeLayer?
private var pulseLayer: CAShapeLayer?
private var isCurrentUser: Bool = false
private let bubbleView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = bubbleCorner
v.layer.shadowColor = UIColor.black.cgColor
v.layer.shadowOffset = CGSize(width: 0, height: 2)
v.layer.shadowRadius = 4
v.layer.shadowOpacity = 0.18
return v
}()
private let avatarImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
iv.layer.cornerRadius = bubbleCorner - borderWidth * 0.6
return iv
}()
private let pointerLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.white.cgColor
return layer
}()
private let dotOuterView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = dotOuterSize / 2
v.layer.shadowColor = UIColor.black.cgColor
v.layer.shadowOffset = CGSize(width: 0, height: 1)
v.layer.shadowRadius = 2
v.layer.shadowOpacity = 0.2
return v
}()
private let dotInnerView: UIView = {
let v = UIView()
v.layer.cornerRadius = dotInnerSize / 2
return v
}()
private let dotCenterView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = 2
return v
}()
// MARK: - Init
override init!(annotation: MAAnnotation!, reuseIdentifier: String!) {
@ -89,34 +132,41 @@ final class MemberAnnotationView: MAAnnotationView {
isEnabled = true
addSubview(containerView)
containerView.addSubview(avatarOuterCircle)
avatarOuterCircle.addSubview(avatarImageView)
containerView.addSubview(nameTagView)
nameTagView.addSubview(nameLabel)
containerView.layer.addSublayer(pointerLayer)
containerView.addSubview(bubbleView)
bubbleView.addSubview(avatarImageView)
containerView.addSubview(dotOuterView)
dotOuterView.addSubview(dotInnerView)
dotInnerView.addSubview(dotCenterView)
}
// MARK: - Configure
func configure(with member: CircleMember) {
self.isEnabled = true
avatarImageView.image = UIImage(named: "UserIcon/\(member.avatar)")
avatarImageView.backgroundColor = member.avatar.isEmpty ? .lightGray : .clear
nameLabel.text = member.name
isCurrentUser = member.isCurrentUser
isEnabled = true
canShowCallout = false
//
if !member.isCurrentUser && gestureRecognizers?.isEmpty ?? true {
if let img = UIImage(named: "UserIcon/\(member.avatar)") {
avatarImageView.image = img
} else {
avatarImageView.image = UIImage(named: "Common/default_avatar")
}
avatarImageView.backgroundColor = member.avatar.isEmpty ? .lightGray : .clear
dotInnerView.backgroundColor = Self.dotColor(for: member)
showsNameTag = !member.isCurrentUser
nameLabel.text = member.name
nameTagView.isHidden = !showsNameTag
//
if gestureRecognizers?.isEmpty ?? true {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tap)
}
if member.isCurrentUser {
configureAsCurrentUser(heading: member.heading)
} else {
configureAsMember()
}
layoutMarker()
}
@objc private func handleTap() {
@ -124,144 +174,94 @@ final class MemberAnnotationView: MAAnnotationView {
onTap?(ann)
}
private func configureAsCurrentUser(heading: Double) {
nameTagView.isHidden = true
stopPulseAnimation()
/// API HomeViewController
func updateHeading(_ heading: Double) {}
let outer = Self.avatarOuterSize
let padding: CGFloat = 30
let size = outer + padding * 2
bounds = CGRect(x: 0, y: 0, width: size, height: size)
centerOffset = .zero
containerView.frame = bounds
// MARK: - Layout
private func layoutMarker() {
let bubble = Self.bubbleSize
let pointerH = Self.pointerHeight
let gap = Self.gapBubbleToDot
let dot = Self.dotOuterSize
let avatarX = (size - outer) / 2
let avatarY = (size - outer) / 2
avatarOuterCircle.frame = CGRect(x: avatarX, y: avatarY,
width: outer, height: outer)
avatarImageView.frame = CGRect(
x: (outer - Self.avatarInnerSize) / 2,
y: (outer - Self.avatarInnerSize) / 2,
width: Self.avatarInnerSize, height: Self.avatarInnerSize
)
let nameH: CGFloat = showsNameTag ? Self.nameTagHeight : 0
let nameGap: CGFloat = showsNameTag ? Self.gapNameToBubble : 0
addHeadingIndicator(heading: heading)
}
private func configureAsMember() {
nameTagView.isHidden = false
headingLayer?.removeFromSuperlayer()
headingLayer = nil
stopPulseAnimation()
//
var nameTagWidth: CGFloat = 0
if showsNameTag {
let text = nameLabel.text ?? ""
let textWidth = (text as NSString).size(withAttributes: [.font: nameLabel.font as Any]).width
let tagWidth = min(max(textWidth + 16, 44), 120)
let totalWidth = max(tagWidth, Self.avatarOuterSize)
let tagTop: CGFloat = 0
let avatarTop = Self.nameTagHeight + 4
let totalHeight = avatarTop + Self.avatarOuterSize
nameTagWidth = min(max(textWidth + Self.nameTagHPadding * 2, 44), 120)
}
let totalWidth = max(bubble, nameTagWidth)
let totalHeight = nameH + nameGap + bubble + pointerH + gap + dot
bounds = CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight)
containerView.frame = bounds
// avatar annotation
let avatarCenterY = avatarTop + Self.avatarOuterSize / 2
centerOffset = CGPoint(x: 0, y: avatarCenterY - totalHeight / 2)
// nameTag
// annotation y
centerOffset = CGPoint(x: 0, y: -(totalHeight / 2 - Self.dotOuterSize / 2))
if showsNameTag {
nameTagView.frame = CGRect(
x: (totalWidth - tagWidth) / 2, y: tagTop,
width: tagWidth, height: Self.nameTagHeight
x: (totalWidth - nameTagWidth) / 2,
y: 0,
width: nameTagWidth,
height: Self.nameTagHeight
)
nameLabel.frame = nameTagView.bounds.insetBy(dx: 6, dy: 0)
nameLabel.frame = nameTagView.bounds.insetBy(dx: Self.nameTagHPadding - 2, dy: 0)
}
// nameTag
avatarOuterCircle.frame = CGRect(
x: (totalWidth - Self.avatarOuterSize) / 2, y: avatarTop,
width: Self.avatarOuterSize, height: Self.avatarOuterSize
let bubbleY = nameH + nameGap
bubbleView.frame = CGRect(
x: (totalWidth - bubble) / 2,
y: bubbleY,
width: bubble,
height: bubble
)
let inner = bubble - Self.borderWidth * 2
avatarImageView.frame = CGRect(
x: (Self.avatarOuterSize - Self.avatarInnerSize) / 2,
y: (Self.avatarOuterSize - Self.avatarInnerSize) / 2,
width: Self.avatarInnerSize, height: Self.avatarInnerSize
x: Self.borderWidth, y: Self.borderWidth,
width: inner, height: inner
)
//
let pointerTop = bubbleY + bubble - 1
let pointerPath = UIBezierPath()
let leftX = (totalWidth - Self.pointerWidth) / 2
pointerPath.move(to: CGPoint(x: leftX, y: pointerTop))
pointerPath.addLine(to: CGPoint(x: leftX + Self.pointerWidth, y: pointerTop))
pointerPath.addLine(to: CGPoint(x: totalWidth / 2, y: pointerTop + pointerH))
pointerPath.close()
pointerLayer.path = pointerPath.cgPath
let dotY = bubbleY + bubble + pointerH + gap
dotOuterView.frame = CGRect(
x: (totalWidth - dot) / 2, y: dotY,
width: dot, height: dot
)
let inset = (Self.dotOuterSize - Self.dotInnerSize) / 2
dotInnerView.frame = CGRect(
x: inset, y: inset,
width: Self.dotInnerSize, height: Self.dotInnerSize
)
let centerSize: CGFloat = 4
let centerInset = (Self.dotInnerSize - centerSize) / 2
dotCenterView.frame = CGRect(
x: centerInset, y: centerInset,
width: centerSize, height: centerSize
)
}
// MARK: - Heading
func updateHeading(_ heading: Double) {
headingLayer?.removeFromSuperlayer()
headingLayer = nil
addHeadingIndicator(heading: heading)
// MARK: - Dot Color
private static func dotColor(for member: CircleMember) -> UIColor {
if member.isCurrentUser {
return currentUserDotColor
}
private func addHeadingIndicator(heading: Double) {
headingLayer?.removeFromSuperlayer()
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let radius: CGFloat = 80
let halfFan = 30.0 * .pi / 180
let centerAngle = CGFloat(heading) * .pi / 180 - .pi / 2
let startAngle = centerAngle - halfFan
let endAngle = centerAngle + halfFan
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius,
startAngle: startAngle, endAngle: endAngle,
clockwise: true)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = containerView.bounds
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.25).cgColor
shapeLayer.strokeColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.4).cgColor
shapeLayer.lineWidth = 1
containerView.layer.insertSublayer(shapeLayer, at: 0)
headingLayer = shapeLayer
}
// MARK: - Pulse Animation
private func startPulseAnimation() {
guard pulseLayer == nil else { return }
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = UIColor(hexStr: "#16B3FF").withAlphaComponent(0.5).cgColor
layer.lineWidth = 2
layer.frame = bounds
containerView.layer.insertSublayer(layer, at: 0)
pulseLayer = layer
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let radius = Self.avatarOuterSize / 2 + 8
let ringRect = CGRect(x: center.x - radius, y: center.y - radius,
width: radius * 2, height: radius * 2)
layer.path = UIBezierPath(ovalIn: ringRect).cgPath
let scale = CABasicAnimation(keyPath: "transform.scale")
scale.fromValue = 0.9
scale.toValue = 1.5
let opacity = CABasicAnimation(keyPath: "opacity")
opacity.fromValue = 0.8
opacity.toValue = 0.0
let group = CAAnimationGroup()
group.animations = [scale, opacity]
group.duration = 1.6
group.repeatCount = .infinity
group.timingFunction = CAMediaTimingFunction(name: .easeOut)
layer.add(group, forKey: "pulse")
}
private func stopPulseAnimation() {
pulseLayer?.removeAllAnimations()
pulseLayer?.removeFromSuperlayer()
pulseLayer = nil
let hash = member.id.utf8.reduce(0) { ($0 &* 31) &+ Int($1) }
let index = abs(hash) % memberDotPalette.count
return memberDotPalette[index]
}
}
#endif

View File

@ -81,7 +81,13 @@ class AccountVC: BaseViewController {
private func deleteAccount() {
DLToast.showLoading()
UserService.deleteAccount().subscribe(onNext: { response in
DLToast.show(text: "申请成功")
DLToast.show(text: "申请成功") {
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
GeYanSdk.deletePreResultCache()
AppContextManager.shared.deleteAccount()
GroupIMService.shared.logout()
AppDelegate.shared.showMainViewController()
}
}, onError: { _ in }).disposed(by: disposeBag)
}