diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 7dd045d2..ef953fe5 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -278,6 +278,7 @@ 30EFF3E82FCA8C7000227D26 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30EFF3E92FCA8C7000227D26 /* AuthenticationServices.framework */; }; 4392B1D9C125D873FCFCDDC9 /* app_catalog.json in Resources */ = {isa = PBXBuildFile; fileRef = 3C79865C3ED165AFDD091C29 /* app_catalog.json */; }; 488FF21773B8E5021D8662B5 /* AppRestrictView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787F87DD1CEBED1A4E1E9FC7 /* AppRestrictView.swift */; }; + 4A69FEABC5F1BA738C35A607 /* ImageCropVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */; }; 4AF6E5954D73E21F31F7A16B /* AppRestrictVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */; }; 557E8D5330187A5C0032AB51 /* BaseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */; }; 55B2179130217D6600784774 /* HomeView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2179030217D6600784774 /* HomeView2.swift */; }; @@ -389,6 +390,7 @@ E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */; }; EA13C1E21EC94958AC9755A8 /* FamilyActivityPickerHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF9D223AB35744F92E0E3E77 /* FamilyActivityPickerHost.swift */; }; F53E5BD11D671E5CED4E7408 /* ManagedSettingsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */; }; + F9CE7D326B4F4F3C1FFAA447 /* ImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E818049FA5A8557C434E37 /* ImageCropView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -713,6 +715,7 @@ 475D33CAFA1E1911EB1F8D9F /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47922567FCD34DBE9186F8FD /* AppCatalogStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppCatalogStore.swift; path = AppRestrict/AppCatalogStore.swift; sourceTree = ""; }; 4CD6B965A124276661A84C4C /* FamilyControls.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FamilyControls.framework; path = System/Library/Frameworks/FamilyControls.framework; sourceTree = SDKROOT; }; + 51E818049FA5A8557C434E37 /* ImageCropView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageCropView.swift; sourceTree = ""; }; 557E8D5230187A5C0032AB51 /* BaseNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationView.swift; sourceTree = ""; }; 55B2179030217D6600784774 /* HomeView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView2.swift; sourceTree = ""; }; 55B2179230218CE100784774 /* GroupMemberView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberView2.swift; sourceTree = ""; }; @@ -806,6 +809,7 @@ AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = ""; }; BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = ""; }; BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = ""; }; + C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageCropVC.swift; sourceTree = ""; }; D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = ""; }; D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DeviceActivityMonitorExtension.swift; path = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift; sourceTree = ""; }; DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.debug.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.debug.xcconfig"; sourceTree = ""; }; @@ -817,21 +821,29 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 30CCDF8E2FE3E63B00F5214A /* sound */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = sound; sourceTree = ""; }; 30CCDF902FE3E63B00F5214A /* video */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = video; sourceTree = ""; }; 30CCE01E2FE3E64700F5214A /* lotties */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = lotties; sourceTree = ""; }; 30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = TTGTagCollectionView; sourceTree = ""; }; @@ -991,6 +1003,7 @@ 305A74DB2FCA8C7000227D26 /* CornerRadiusView */, 305A74E22FCA8C7000227D26 /* ImagePicker */, 305A74E62FCA8C7000227D26 /* TextTableViewCell */, + B2DDB0083141D63A991C9FEE /* ImageCrop */, ); path = Component; sourceTree = ""; @@ -2110,6 +2123,16 @@ path = Pods; sourceTree = ""; }; + B2DDB0083141D63A991C9FEE /* ImageCrop */ = { + isa = PBXGroup; + children = ( + 51E818049FA5A8557C434E37 /* ImageCropView.swift */, + C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */, + ); + name = ImageCrop; + path = ImageCrop; + sourceTree = ""; + }; B79BE06D49443FE82A811D5C /* LockDistract */ = { isa = PBXGroup; children = ( @@ -2704,6 +2727,8 @@ E3D4129BDA33540BA4E1292E /* LockDistractVC.swift in Sources */, E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */, DB67C2EB2521042829A3DBA5 /* PairGuideStepsView.swift in Sources */, + F9CE7D326B4F4F3C1FFAA447 /* ImageCropView.swift in Sources */, + 4A69FEABC5F1BA738C35A607 /* ImageCropVC.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json new file mode 100644 index 00000000..9b83bf73 --- /dev/null +++ b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "area_badge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "area_badge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png new file mode 100644 index 00000000..6f64aeb3 Binary files /dev/null and b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png differ diff --git a/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@3x.png b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@3x.png new file mode 100644 index 00000000..e31abd98 Binary files /dev/null and b/QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@3x.png differ diff --git a/QuickLocation/Component/ImageCrop/ImageCropVC.swift b/QuickLocation/Component/ImageCrop/ImageCropVC.swift new file mode 100644 index 00000000..225395a4 --- /dev/null +++ b/QuickLocation/Component/ImageCrop/ImageCropVC.swift @@ -0,0 +1,54 @@ +// +// ImageCropVC.swift +// QuickLocation +// + +import UIKit + +final class ImageCropVC: BaseViewController { + + override var isNavigationBarHidden: Bool { true } + override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent } + + var onConfirm: ((UIImage) -> Void)? + var onCancel: (() -> Void)? + + private let image: UIImage + private var rootView: ImageCropView! + + init(image: UIImage) { + self.image = image + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .fullScreen + modalPresentationCapturesStatusBarAppearance = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + rootView = ImageCropView(frame: UIScreen.main.bounds) + view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + rootView.setImage(image) + rootView.onCancel = { [weak self] in + self?.dismiss(animated: true) { + self?.onCancel?() + } + } + rootView.onConfirm = { [weak self] in + guard let self else { return } + guard let cropped = self.rootView.croppedImage() else { + DLToast.show(text: "图片处理失败") + return + } + self.dismiss(animated: true) { + self.onConfirm?(cropped) + } + } + } +} diff --git a/QuickLocation/Component/ImageCrop/ImageCropView.swift b/QuickLocation/Component/ImageCrop/ImageCropView.swift new file mode 100644 index 00000000..06261d9e --- /dev/null +++ b/QuickLocation/Component/ImageCrop/ImageCropView.swift @@ -0,0 +1,314 @@ +// +// ImageCropView.swift +// QuickLocation +// + +import UIKit + +final class ImageCropView: UIView, UIScrollViewDelegate { + + var onCancel: (() -> Void)? + var onConfirm: (() -> Void)? + + private let scrollView = UIScrollView() + private let imageView = UIImageView() + private let overlayView = ImageCropOverlayView() + private let topBar = UIView() + private let bottomBar = UIView() + + private var cropRect: CGRect = .zero + private var fittedCropRect: CGRect = .zero + private var pendingFit = false + private var userAdjusted = false + private var isFitting = false + + private let bottomBarHeight: CGFloat = 56 + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .black + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setImage(_ image: UIImage) { + imageView.image = image.fixOrientation() + pendingFit = true + setNeedsLayout() + } + + func rotateClockwise() { + guard let image = imageView.image else { return } + setImage(image.rotatedClockwise()) + } + + func croppedImage() -> UIImage? { + guard let image = imageView.image, imageView.bounds.width > 0, cropRect.width > 0 else { + return nil + } + let zoom = scrollView.zoomScale + guard zoom > 0 else { return nil } + + let origin = CGPoint( + x: (scrollView.contentOffset.x + cropRect.minX) / zoom, + y: (scrollView.contentOffset.y + cropRect.minY) / zoom + ) + let size = CGSize( + width: cropRect.width / zoom, + height: cropRect.height / zoom + ) + let rect = CGRect(origin: origin, size: size) + + guard let cgImage = image.cgImage, imageView.bounds.width > 0, imageView.bounds.height > 0 else { + return nil + } + let scaleX = CGFloat(cgImage.width) / imageView.bounds.width + let scaleY = CGFloat(cgImage.height) / imageView.bounds.height + var pixelRect = CGRect( + x: rect.origin.x * scaleX, + y: rect.origin.y * scaleY, + width: rect.width * scaleX, + height: rect.height * scaleY + ).integral + + let bounds = CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height) + pixelRect = pixelRect.intersection(bounds) + guard pixelRect.width >= 1, pixelRect.height >= 1, + let cropped = cgImage.cropping(to: pixelRect) else { + return nil + } + return UIImage(cgImage: cropped, scale: 1, orientation: .up) + } + + override func layoutSubviews() { + super.layoutSubviews() + scrollView.frame = bounds + overlayView.frame = bounds + + let topSafe = safeAreaInsets.top + let bottomSafe = safeAreaInsets.bottom + topBar.frame = CGRect(x: 0, y: 0, width: bounds.width, height: topSafe + 44) + bottomBar.frame = CGRect( + x: 0, + y: bounds.height - bottomSafe - bottomBarHeight, + width: bounds.width, + height: bottomSafe + bottomBarHeight + ) + + let side = bounds.width + let availableTop = topBar.frame.maxY + let availableBottom = bottomBar.frame.minY + let originY = availableTop + max(0, (availableBottom - availableTop - side) / 2) + cropRect = CGRect(x: 0, y: originY, width: side, height: side) + overlayView.cropRect = cropRect + applyCropInsets() + + if pendingFit, bounds.width > 0, imageView.image != nil { + pendingFit = false + userAdjusted = false + fitImageToCrop() + } else if !userAdjusted, cropRect != fittedCropRect, imageView.image != nil { + fitImageToCrop() + } + } + + // MARK: - UIScrollViewDelegate + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + imageView + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if !isFitting { userAdjusted = true } + } + + func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { + if !isFitting { userAdjusted = true } + } + + // MARK: - Private + + private func setupUI() { + scrollView.delegate = self + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + scrollView.bounces = true + scrollView.alwaysBounceVertical = true + scrollView.alwaysBounceHorizontal = true + scrollView.bouncesZoom = true + scrollView.contentInsetAdjustmentBehavior = .never + scrollView.backgroundColor = .black + addSubview(scrollView) + + imageView.contentMode = .scaleToFill + imageView.isUserInteractionEnabled = true + scrollView.addSubview(imageView) + + overlayView.isUserInteractionEnabled = false + addSubview(overlayView) + + addSubview(topBar) + addSubview(bottomBar) + + let closeBtn = UIButton(type: .custom) + closeBtn.setImage(UIImage(named: "PhotoPicker/btn_close"), for: .normal) + closeBtn.addTarget(self, action: #selector(tapCancel), for: .touchUpInside) + topBar.addSubview(closeBtn) + closeBtn.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + closeBtn.leadingAnchor.constraint(equalTo: topBar.leadingAnchor, constant: 8), + closeBtn.bottomAnchor.constraint(equalTo: topBar.bottomAnchor), + closeBtn.widthAnchor.constraint(equalToConstant: 44), + closeBtn.heightAnchor.constraint(equalToConstant: 44) + ]) + + let cancelBtn = UIButton(type: .custom) + cancelBtn.setTitle("取消", for: .normal) + cancelBtn.setTitleColor(.white, for: .normal) + cancelBtn.titleLabel?.font = .systemFont(ofSize: 16) + cancelBtn.addTarget(self, action: #selector(tapCancel), for: .touchUpInside) + bottomBar.addSubview(cancelBtn) + + let rotateBtn = UIButton(type: .custom) + let rotateConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .regular) + rotateBtn.setImage(UIImage(systemName: "rotate.right", withConfiguration: rotateConfig), for: .normal) + rotateBtn.tintColor = .white + rotateBtn.addTarget(self, action: #selector(tapRotate), for: .touchUpInside) + bottomBar.addSubview(rotateBtn) + + let confirmBtn = UIButton(type: .custom) + confirmBtn.setTitle("确定", for: .normal) + confirmBtn.setTitleColor(.white, for: .normal) + confirmBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + confirmBtn.backgroundColor = UIColor(hexStr: "#07C160") + confirmBtn.layer.cornerRadius = 8 + confirmBtn.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside) + bottomBar.addSubview(confirmBtn) + + cancelBtn.translatesAutoresizingMaskIntoConstraints = false + rotateBtn.translatesAutoresizingMaskIntoConstraints = false + confirmBtn.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + cancelBtn.leadingAnchor.constraint(equalTo: bottomBar.leadingAnchor, constant: 20), + cancelBtn.topAnchor.constraint(equalTo: bottomBar.topAnchor, constant: 8), + cancelBtn.heightAnchor.constraint(equalToConstant: 40), + + rotateBtn.centerXAnchor.constraint(equalTo: bottomBar.centerXAnchor), + rotateBtn.centerYAnchor.constraint(equalTo: cancelBtn.centerYAnchor), + rotateBtn.widthAnchor.constraint(equalToConstant: 44), + rotateBtn.heightAnchor.constraint(equalToConstant: 44), + + confirmBtn.trailingAnchor.constraint(equalTo: bottomBar.trailingAnchor, constant: -16), + confirmBtn.centerYAnchor.constraint(equalTo: cancelBtn.centerYAnchor), + confirmBtn.widthAnchor.constraint(equalToConstant: 72), + confirmBtn.heightAnchor.constraint(equalToConstant: 36) + ]) + } + + private func fitImageToCrop() { + guard let image = imageView.image, cropRect.width > 0 else { return } + let imageSize = image.size + guard imageSize.width > 0, imageSize.height > 0 else { return } + + isFitting = true + scrollView.minimumZoomScale = 1 + scrollView.maximumZoomScale = 1 + scrollView.zoomScale = 1 + imageView.transform = .identity + imageView.frame = CGRect(origin: .zero, size: imageSize) + scrollView.contentSize = imageSize + + applyCropInsets() + + let minScale = max(cropRect.width / imageSize.width, cropRect.height / imageSize.height) + scrollView.minimumZoomScale = minScale + scrollView.maximumZoomScale = max(minScale * 4, 4) + scrollView.zoomScale = minScale + + let scaled = CGSize(width: imageSize.width * minScale, height: imageSize.height * minScale) + scrollView.contentOffset = CGPoint( + x: (scaled.width - cropRect.width) / 2 - scrollView.contentInset.left, + y: (scaled.height - cropRect.height) / 2 - scrollView.contentInset.top + ) + fittedCropRect = cropRect + isFitting = false + } + + private func applyCropInsets() { + scrollView.contentInset = UIEdgeInsets( + top: cropRect.minY, + left: cropRect.minX, + bottom: max(0, bounds.height - cropRect.maxY), + right: max(0, bounds.width - cropRect.maxX) + ) + } + + @objc private func tapCancel() { + onCancel?() + } + + @objc private func tapRotate() { + rotateClockwise() + } + + @objc private func tapConfirm() { + onConfirm?() + } +} + +private final class ImageCropOverlayView: UIView { + + var cropRect: CGRect = .zero { + didSet { updateMaskPath() } + } + + private let dimLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.fillRule = .evenOdd + layer.fillColor = UIColor.black.withAlphaComponent(0.55).cgColor + return layer + }() + + override init(frame: CGRect) { + super.init(frame: frame) + isUserInteractionEnabled = false + backgroundColor = .clear + layer.addSublayer(dimLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + dimLayer.frame = bounds + updateMaskPath() + } + + private func updateMaskPath() { + let path = UIBezierPath(rect: bounds) + if cropRect.width > 0, cropRect.height > 0 { + path.append(UIBezierPath(rect: cropRect)) + } + dimLayer.path = path.cgPath + } +} + +private extension UIImage { + func rotatedClockwise() -> UIImage { + let newSize = CGSize(width: size.height, height: size.width) + let format = UIGraphicsImageRendererFormat.default() + format.scale = scale + format.opaque = false + let renderer = UIGraphicsImageRenderer(size: newSize, format: format) + return renderer.image { ctx in + ctx.cgContext.translateBy(x: newSize.width, y: 0) + ctx.cgContext.rotate(by: .pi / 2) + draw(in: CGRect(origin: .zero, size: size)) + } + } +} diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift index a1fce517..36d335c8 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationHeaderView.swift @@ -5,48 +5,24 @@ import UIKit -/// 查位置页共用顶部:hero + 使用人数 + 双标题 + 跑马灯 +/// 查位置页共用顶部:hero + 双标题 final class SearchLocationHeaderView: UIView { - let usageLab = UILabel() let headlineLab = UILabel() let subheadLab = UILabel() - let marqueeScrollView = UIScrollView() - - /// 猫插画 + 装饰合图;有切图时设 image 并清空 backgroundColor let heroImageView = UIImageView() override init(frame: CGRect) { super.init(frame: frame) setupUI() - setupMarquee() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func startMarqueeAnimation() { - marqueeScrollView.isHidden = true - return - - marqueeScrollView.layer.removeAllAnimations() - marqueeScrollView.contentOffset.x = 0 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } - let contentW = self.marqueeScrollView.contentSize.width - guard contentW > self.marqueeScrollView.bounds.width else { return } - let duration = contentW / 100 - UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear, .repeat]) { - self.marqueeScrollView.contentOffset.x = contentW - self.marqueeScrollView.bounds.width - } - } - } - private func setupUI() { addSubview(heroImageView) - addSubview(usageLab) addSubview(headlineLab) addSubview(subheadLab) - addSubview(marqueeScrollView) heroImageView.backgroundColor = .clear heroImageView.contentMode = .scaleAspectFit @@ -57,25 +33,12 @@ final class SearchLocationHeaderView: UIView { .width(288) .height(190) - let count = Int.random(in: 8000...15000) - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.groupingSeparator = "," - let countText = formatter.string(from: NSNumber(value: count)) ?? "\(count)" - usageLab.text = "有\(countText)人正在使用此功能" - usageLab.font = .systemFont(ofSize: 14, weight: .medium) - usageLab.textColor = UIColor(hexStr: "#999999") - usageLab.textAlignment = .center - usageLab.layoutChain - .topToBottomOfView(heroImageView, offset: 42) - .centerX() - headlineLab.text = "实时查看TA的位置" headlineLab.font = FontManager.boboBold(30) headlineLab.textColor = UIColor(hexStr: "#293445") headlineLab.textAlignment = .center headlineLab.layoutChain - .topToBottomOfView(usageLab, offset: 37) + .topToBottomOfView(heroImageView, offset: 67) .centerX() subheadLab.text = "为你安心守护出行安全" @@ -83,82 +46,8 @@ final class SearchLocationHeaderView: UIView { subheadLab.textColor = UIColor(hexStr: "#293445") subheadLab.textAlignment = .center subheadLab.layoutChain - .topToBottomOfView(headlineLab, offset: 18) + .topToBottomOfView(headlineLab, offset: 20) .centerX() .bottom() - - marqueeScrollView.isHidden = true - marqueeScrollView.backgroundColor = .clear - marqueeScrollView.showsHorizontalScrollIndicator = false - marqueeScrollView.isScrollEnabled = false - } - - private func randomPhoneNumber() -> String { - let prefixes = [ - "134","135","136","137","138","139","147","150","151","152","157","158","159","178","182","183","184","187","188","198", - "130","131","132","145","155","156","166","175","176","185","186","196", - "133","149","153","173","177","180","181","189","191","199" - ] - let prefix = prefixes.randomElement()! - var suffix = "" - for _ in 0..<8 { suffix.append("\(Int.random(in: 0...9))") } - return prefix + suffix - } - - private func setupMarquee() { - let surnames = ["张", "李", "王", "陈", "刘", "杨", "赵", "黄", "周", "吴", - "林", "何", "马", "胡", "郑", "梁", "谢", "宋", "唐", "韩"] - let container = UIView() - container.backgroundColor = .clear - marqueeScrollView.addSubview(container) - - var prevView: UIView? - for _ in 0..<10 { - let iconIndex = Int.random(in: 1...15) - let surname = surnames.randomElement() ?? "张" - let maskedName = "\(surname)**" - let phone = randomPhoneNumber() - let maskedPhone = phone.prefix(3) + "******" + phone.suffix(2) - - let itemView = UIView() - itemView.backgroundColor = UIColor(hexStr: "#EFF9FF") - itemView.cornerRadius = 8 - container.addSubview(itemView) - - let avatar = UIImageView(image: UIImage(named: "UserIcon/\(iconIndex)")) - avatar.contentMode = .scaleAspectFill - avatar.backgroundColor = UIColor(hexStr: "#E0E0E0") - avatar.cornerRadius = 12 - avatar.clipsToBounds = true - itemView.addSubview(avatar) - - let label = UILabel() - label.font = .systemFont(ofSize: 13, weight: .medium) - let fullText = "\(maskedName) 定位到了 \(maskedPhone)" - let attr = NSMutableAttributedString(string: fullText) - attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), - range: NSRange(fullText.range(of: maskedName)!, in: fullText)) - if let phoneRange = fullText.range(of: maskedPhone) { - attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), - range: NSRange(phoneRange, in: fullText)) - } - label.attributedText = attr - itemView.addSubview(label) - - avatar.layoutChain.left(10).centerY().width(24).height(24) - label.layoutChain.leftToRightOfView(avatar, offset: 6).centerY().right(10) - itemView.layoutChain.centerY().height(30) - - if let prev = prevView { - itemView.layoutChain.leftToRightOfView(prev, offset: 16) - } else { - itemView.layoutChain.left() - } - prevView = itemView - } - - if let last = prevView { - container.layoutChain.edges().heightToView(marqueeScrollView).rightToView(last) - } } } diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift index 0e620865..16b487b3 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultVC.swift @@ -28,11 +28,13 @@ class SearchLocationResultVC: BaseViewController { reactiveAction() applyResultPresentation() requestPhoneArea() + if code == 0 || code == 20005 { + requestGroupInfo() + } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - rootView.startMarqueeAnimation() applyResultPresentation() } @@ -45,9 +47,14 @@ class SearchLocationResultVC: BaseViewController { rootView.actionBtn.rx.tap.subscribe(onNext: { _ in switch self.code { case 0: + var userInfo = self.memberData + let selectedKey = self.rootView.currentGroupKey + if !selectedKey.isEmpty { + userInfo["group_key"] = selectedKey + } NotificationCenter.default.post(name: .ShowMemberLocationNotification, object: nil, - userInfo: self.memberData) + userInfo: userInfo) self.navigationController?.popToRootViewController(animated: true) if let tab = self.tabBarController as? MainTabBarController ?? UIViewController.topMost?.tabBarController as? MainTabBarController { @@ -79,49 +86,90 @@ class SearchLocationResultVC: BaseViewController { } } - private func shareApp() { - let text = "快来极速定位,实时守护家人出行安全" - let activity = UIActivityViewController(activityItems: [text], applicationActivities: nil) - present(activity, animated: true) - } - private func handleCode() { rootView.actionBtn.isHidden = true - rootView.inviteView.isHidden = true switch code { case 0: rootView.configureResult( status: "定位成功", - disclaimer: "点击去查看可获得具体位置", - showInviteRow: false + disclaimer: "选择圈子查看可获得具体位置", + showWarning: false, + showGroups: hasSharedGroups ) rootView.actionBtn.setTitle("去查看", for: .normal) rootView.actionBtn.isHidden = false case 20004: rootView.configureResult( status: "用户未注册", - disclaimer: "根据最新的相关法规,需用户双方均安装该APP才可实现定位,未安装APP的用户将不再提供具体定位功能。", - showInviteRow: false + disclaimer: "您查询的用户未注册账号,无法提供具体定位,请邀请他加入您的圈子吧", + showWarning: true, + showGroups: false ) rootView.actionBtn.setTitle("分享", for: .normal) rootView.actionBtn.isHidden = false case 20005: rootView.configureResult( status: "定位成功", - disclaimer: "根据最新的相关法规,您查询的用户与您不在同一个圈子时, 不再提供具体的位置定位,邀请ta加入你的圈子后可随时查看位置。", - showInviteRow: false + disclaimer: "您查询的用户与您不在同一个圈子,无法提供具体定位,请邀请他加入您的圈子吧", + showWarning: true, + showGroups: false ) rootView.actionBtn.setTitle("邀请TA加入", for: .normal) rootView.actionBtn.isHidden = false - if groupModel == nil { - requestGroupInfo() - } default: break } } + private var hasSharedGroups: Bool { + guard let model = groupModel else { return false } + return !sharedGroups(from: model.groups).isEmpty + } + + /// 只保留与被查找用户的共同圈子。 + private func sharedGroups(from myGroups: [GroupInfoModel]) -> [GroupInfoModel] { + let keys = sharedGroupKeys() + guard !keys.isEmpty else { return [] } + let keySet = Set(keys) + return myGroups.filter { keySet.contains($0.group_key) } + } + + private func sharedGroupKeys() -> [String] { + var keys: [String] = [] + keys.append(contentsOf: groupKeys(in: memberData["common_groups"])) + keys.append(contentsOf: groupKeys(in: memberData["groups"])) + if let list = memberData["group_keys"] as? [Any] { + keys.append(contentsOf: list.compactMap(stringValue).filter { !$0.isEmpty }) + } + let single = stringValue(memberData["group_key"]) + if !single.isEmpty { keys.append(single) } + + var seen = Set() + return keys.filter { seen.insert($0).inserted } + } + + private func groupKeys(in value: Any?) -> [String] { + guard let list = value as? [Any] else { return [] } + return list.compactMap { item in + if let dict = item as? [String: Any] { + return stringValue(dict["group_key"]) + } + return stringValue(item) + }.filter { !$0.isEmpty } + } + + private func stringValue(_ value: Any?) -> String { + switch value { + case let text as String: + return text.trimmingCharacters(in: .whitespacesAndNewlines) + case let number as NSNumber: + return number.stringValue + default: + return "" + } + } + private func requestPhoneArea() { SystemService.phoneArea(phone: phone).subscribe(onNext: { response in guard let data = response.data else { return } @@ -136,6 +184,15 @@ class SearchLocationResultVC: BaseViewController { GroupService.groupInfo().subscribe { [weak self] response in guard let self = self, let model = response.model else { return } self.groupModel = model + if self.code == 0 { + let shared = self.sharedGroups(from: model.groups) + let preferred = self.stringValue(self.memberData["group_key"]) + let selected = shared.contains(where: { $0.group_key == preferred }) + ? preferred + : (shared.first?.group_key ?? "") + self.rootView.reloadGroups(shared, selectedKey: selected) + } + self.handleCode() if showPicker { self.presentGroupPicker(with: model) } diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift index 0ca442fd..ea835eeb 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationResultView.swift @@ -4,17 +4,28 @@ // import UIKit -import Lottie class SearchLocationResultView: UIView { let headerView = SearchLocationHeaderView() + var onSelectGroup: ((String) -> Void)? + + private var chipButtons: [UIButton] = [] + private var selectedGroupKey: String = "" + private var chipHeightConstraint: NSLayoutConstraint? + private var chipTopConstraint: NSLayoutConstraint? + private func setupUI() { addSubview(navBgView) addSubview(navView) addSubview(scrollView) - + addSubview(actionBtn) + actionBtn.layoutChain + .edgesHorzontal(20) + .height(56) + .bottom(kSafeBottomMargin + 12) + navBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160 / 375) @@ -28,22 +39,33 @@ class SearchLocationResultView: UIView { .edges(excludingEdge: .top) } - func startMarqueeAnimation() { - headerView.startMarqueeAnimation() - } - - func configureResult(status: String, disclaimer: String, showInviteRow: Bool) { + func configureResult(status: String, disclaimer: String, showWarning: Bool, showGroups: Bool) { statusPillLab.text = status disclaimerLab.text = disclaimer - disclaimerLab.textAlignment = disclaimer.contains("去查看") ? .center : .left - disclaimerIcon.isHidden = disclaimer.contains("去查看") - inviteView.isHidden = !showInviteRow - // 隐藏时高度归零,避免仍占 72pt 把卡片撑高 - inviteView.layoutChain - .topToBottomOfView(disclaimerLab, offset: showInviteRow ? 16 : 0) - .height(showInviteRow ? 72 : 0) + disclaimerLab.textAlignment = showWarning ? .left : .center + disclaimerIcon.isHidden = !showWarning + disclaimerRow.alignment = showWarning ? .top : .center + chipScrollView.isHidden = !showGroups + chipTopConstraint?.constant = showGroups ? 16 : 0 + chipHeightConstraint?.constant = showGroups ? 32 : 0 } + func reloadGroups(_ groups: [GroupInfoModel], selectedKey: String) { + selectedGroupKey = selectedKey + chipButtons.forEach { $0.removeFromSuperview() } + chipButtons.removeAll() + chipStack.arrangedSubviews.forEach { $0.removeFromSuperview() } + + for group in groups { + let btn = makeChip(title: group.name, groupKey: group.group_key) + applyChipStyle(btn, selected: group.group_key == selectedKey) + chipStack.addArrangedSubview(btn) + chipButtons.append(btn) + } + } + + var currentGroupKey: String { selectedGroupKey } + func updateSubViews() { normalResultContentView.isHidden = false } @@ -52,6 +74,7 @@ class SearchLocationResultView: UIView { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") iv.contentMode = .scaleAspectFill + iv.isHidden = true return iv }() @@ -75,35 +98,38 @@ class SearchLocationResultView: UIView { contentView.addSubview(resultCard) resultCard.layoutChain - .topToBottomOfView(headerView, offset: 20) - .edgesHorzontal(15) + .topToBottomOfView(headerView, offset: 34) + .edgesHorzontal(20) - contentView.addSubview(actionBtn) - actionBtn.layoutChain - .topToBottomOfView(resultCard, offset: 24) - .edgesHorzontal(30) - .height(56) - .bottom(kSafeBottomMargin + 30) + contentView.addSubview(areaBadgeView) + areaBadgeView.layoutChain + .topToView(resultCard, offset: -15) + .centerX(contentView, offset: 70) + .width(74) + .height(30) + .bottom() return view }() lazy var resultCard: UIView = { let card = UIView() - card.backgroundColor = UIColor(hexStr: "#EFF9FF") - card.cornerRadius = 16 + card.backgroundColor = UIColor(hexStr: "#EFFAFF") + card.cornerRadius = 28 - card.addSubview(resultStackView) - resultStackView.layoutChain.edges() - resultStackView.addArrangedSubview(normalResultContentView) + card.addSubview(normalResultContentView) + normalResultContentView.layoutChain + .top(22) + .edgesHorzontal() + .bottom() return card }() - private lazy var resultStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - return stackView + private lazy var areaBadgeView: UIImageView = { + let iv = UIImageView(image: UIImage(named: "SearchLocation/area_badge")) + iv.contentMode = .scaleAspectFit + return iv }() private lazy var normalResultContentView: UIView = { @@ -116,51 +142,41 @@ class SearchLocationResultView: UIView { locationRow.addSubview(pinIcon) locationRow.addSubview(phoneAreaLab) -// locationRow.addSubview(areaBlurView) - pinIcon.layoutChain.left().centerY().width(18).height(18) phoneAreaLab.layoutChain .leftToRightOfView(pinIcon, offset: 6) .right() .top() .bottom() -// areaBlurView.layoutChain -// .topToView(phoneAreaLab, offset: -2) -// .bottomToView(phoneAreaLab, offset: 2) -// .leftToView(phoneAreaLab, offset: -4) -// .rightToView(phoneAreaLab, offset: 4) - locationRow.layoutChain.top(20).centerX() + locationRow.layoutChain.top().centerX() let pillContainer = UIView() pillContainer.backgroundColor = .white - pillContainer.cornerRadius = 14 + pillContainer.cornerRadius = 16 contentView.addSubview(pillContainer) pillContainer.addSubview(statusPillLab) - statusPillLab.layoutChain.edgesHorzontal(58).edgesVertical(13) + statusPillLab.layoutChain.edgesHorzontal(12).edgesVertical(14) pillContainer.layoutChain - .topToBottomOfView(locationRow, offset: 11) - .centerX() + .topToBottomOfView(locationRow, offset: 14) + .edgesHorzontal(16) .height(48) - contentView.addSubview(disclaimerIcon) - disclaimerIcon.layoutChain - .topToBottomOfView(pillContainer, offset: 14) - .left(24) - .width(12) - .height(12) + contentView.addSubview(disclaimerRow) + disclaimerRow.layoutChain + .topToBottomOfView(pillContainer, offset: 12) + .edgesHorzontal(20) - contentView.addSubview(disclaimerLab) - disclaimerLab.layoutChain - .topToBottomOfView(pillContainer, offset: 11) - .leftToRightOfView(disclaimerIcon, offset: 3) - .right(23) - - contentView.addSubview(inviteView) - inviteView.layoutChain - .topToBottomOfView(disclaimerLab, offset: 0) - .edgesHorzontal(12) - .height(0) - .bottom(16) + contentView.addSubview(chipScrollView) + chipScrollView.translatesAutoresizingMaskIntoConstraints = false + chipTopConstraint = chipScrollView.topAnchor.constraint(equalTo: disclaimerRow.bottomAnchor, constant: 16) + chipHeightConstraint = chipScrollView.heightAnchor.constraint(equalToConstant: 32) + NSLayoutConstraint.activate([ + chipTopConstraint!, + chipHeightConstraint!, + chipScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + chipScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + chipScrollView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20) + ]) return contentView }() @@ -172,15 +188,6 @@ class SearchLocationResultView: UIView { return label }() - lazy var areaBlurView: UIVisualEffectView = { - let view = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - view.isUserInteractionEnabled = false - view.isHidden = true - view.cornerRadius = 4 - view.clipsToBounds = true - return view - }() - lazy var statusPillLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 16, weight: .medium) @@ -192,6 +199,7 @@ class SearchLocationResultView: UIView { private lazy var disclaimerIcon: UIImageView = { let iv = UIImageView(image: UIImage(named: "SearchLocation/warning")) iv.contentMode = .scaleAspectFit + iv.layoutChain.width(12).height(12) return iv }() @@ -203,6 +211,14 @@ class SearchLocationResultView: UIView { return label }() + private lazy var disclaimerRow: UIStackView = { + let stack = UIStackView(arrangedSubviews: [disclaimerIcon, disclaimerLab]) + stack.axis = .horizontal + stack.alignment = .top + stack.spacing = 4 + return stack + }() + lazy var actionBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitleColor(.white, for: .normal) @@ -215,46 +231,64 @@ class SearchLocationResultView: UIView { var inviteBtn: UIButton { actionBtn } - lazy var inviteView: UIView = { - let view = UIView() - view.backgroundColor = .white - view.cornerRadius = 12 - view.isHidden = true - - view.addSubview(successBtn) - successBtn.layoutChain.right(12).centerY().width(90).height(36) - - view.addSubview(successTipsLab) - successTipsLab.layoutChain.topToCenterYOfView(view).left(12) - - view.addSubview(phoneLab) - phoneLab.layoutChain.bottomToCenterYOfView(view).left(12) - + private lazy var chipScrollView: UIScrollView = { + let view = UIScrollView() + view.showsHorizontalScrollIndicator = false + view.alwaysBounceHorizontal = true + view.addSubview(chipStack) + chipStack.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + chipStack.topAnchor.constraint(equalTo: view.contentLayoutGuide.topAnchor), + chipStack.leadingAnchor.constraint(equalTo: view.contentLayoutGuide.leadingAnchor), + chipStack.bottomAnchor.constraint(equalTo: view.contentLayoutGuide.bottomAnchor), + chipStack.trailingAnchor.constraint(equalTo: view.contentLayoutGuide.trailingAnchor), + chipStack.heightAnchor.constraint(equalTo: view.frameLayoutGuide.heightAnchor) + ]) return view }() - lazy var phoneLab: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 16, weight: .bold) - label.textColor = UIColor(hexStr: "#333333") - return label + private lazy var chipStack: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.spacing = 8 + stack.alignment = .fill + return stack }() - lazy var successTipsLab: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 10, weight: .medium) - label.textColor = UIColor(hexStr: "#999999") - return label - }() - - lazy var successBtn: UIButton = { + private func makeChip(title: String, groupKey: String) -> UIButton { let btn = UIButton(type: .custom) - btn.setTitleColor(.white, for: .normal) - btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) - btn.backgroundColor = UIColor(hexStr: "#16B3FF") - btn.cornerRadius = 18 + btn.setTitle(title, for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 13, weight: .medium) + btn.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) + btn.layer.cornerRadius = 8 + btn.layer.borderWidth = 1 + btn.accessibilityIdentifier = groupKey + btn.addTarget(self, action: #selector(tapChip(_:)), for: .touchUpInside) + btn.setContentHuggingPriority(.required, for: .horizontal) + btn.setContentCompressionResistancePriority(.required, for: .horizontal) return btn - }() + } + + private func applyChipStyle(_ btn: UIButton, selected: Bool) { + if selected { + btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) + btn.backgroundColor = .white + btn.layer.borderColor = UIColor(hexStr: "#16B3FF").cgColor + } else { + btn.setTitleColor(UIColor(hexStr: "#C0C4CC"), for: .normal) + btn.backgroundColor = UIColor(hexStr: "#F2F3F5") + btn.layer.borderColor = UIColor.clear.cgColor + } + } + + @objc private func tapChip(_ sender: UIButton) { + let key = sender.accessibilityIdentifier ?? "" + selectedGroupKey = key + for btn in chipButtons { + applyChipStyle(btn, selected: btn === sender) + } + onSelectGroup?(key) + } override init(frame: CGRect) { super.init(frame: .zero) diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift index 52b45280..52ed0d29 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationVC.swift @@ -37,36 +37,32 @@ class SearchLocationVC: BaseViewController { } self.rootView.phoneInputTF.resignFirstResponder() self.requestSearchPhone(phone: phone) - self.rootView.searchProgressView.isHidden = false - self.rootView.playVideo { - self.rootView.videoView.isHidden = true - self.rootView.searchProgressView.isHidden = true - - guard self.code != -999 else { - DLToast.show(text: "发生未知错误,请重试") - return - } - - if self.code == 0 { - AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, - "code": self.code, - "memberData": self.memberData]) - } - else { - AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, "code": self.code]) - } - } +// self.rootView.searchProgressView.isHidden = false +// self.rootView.playVideo { +// self.rootView.videoView.isHidden = true +// self.rootView.searchProgressView.isHidden = true +// +// guard self.code != -999 else { +// DLToast.show(text: "发生未知错误,请重试") +// return +// } +// +// if self.code == 0 { +// AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, +// "code": self.code, +// "memberData": self.memberData]) +// } +// else { +// AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, "code": self.code]) +// } +// } }).disposed(by: disposeBag) } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - rootView.startMarqueeAnimation() - } - + // MARK: - 查找号码 private func requestSearchPhone(phone: String) { + DLToast.showLoading() SystemService.search(op_type: "phone", number: phone).subscribe(onNext: { response in self.code = 0 @@ -81,9 +77,14 @@ class SearchLocationVC: BaseViewController { if let data = response.data { self.memberData = data } + + AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, + "code": self.code, + "memberData": self.memberData]) }, onError: { (error) in guard let code = error.underlyingError?.code else { return } self.code = code + AppRouter.push(Route.searchLocationResult, userInfo: ["phone": phone, "code": self.code]) }).disposed(by: disposeBag) } } diff --git a/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift b/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift index 2f261d39..90bf44bb 100644 --- a/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift +++ b/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift @@ -8,8 +8,6 @@ import RxSwift import RxCocoa import AVFoundation import Lottie -import Contacts -import ContactsUI import URLNavigator class SearchLocationView: UIView { @@ -66,17 +64,6 @@ class SearchLocationView: UIView { .edges(excludingEdge: .top) } - func startMarqueeAnimation() { - headerView.startMarqueeAnimation() - } - - override func willMove(toWindow newWindow: UIWindow?) { - super.willMove(toWindow: newWindow) - if newWindow != nil { - startMarqueeAnimation() - } - } - lazy var navBgView: UIImageView = { let iv = UIImageView() iv.image = UIImage(named: "Common/navBar_bg_2") @@ -108,8 +95,8 @@ class SearchLocationView: UIView { contentView.addSubview(searchInputView) searchInputView.layoutChain - .topToBottomOfView(headerView, offset: 45) - .edgesHorzontal(15) + .topToBottomOfView(headerView, offset: 70) + .edgesHorzontal(20) .bottom(kSafeBottomMargin + 30, relation: .greaterThanOrEqual) return view @@ -169,80 +156,66 @@ class SearchLocationView: UIView { lazy var searchInputView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#EFFAFF") - view.cornerRadius = 40 + view.cornerRadius = 28 let inputView = UIView() inputView.backgroundColor = .white - inputView.cornerRadius = 14 + inputView.cornerRadius = 16 view.addSubview(inputView) inputView.layoutChain .top(20) - .edgesHorzontal(15) - .height(44) + .edgesHorzontal(16) + .height(52) - let contactsBtn = UIButton() - contactsBtn.setTitle("通讯录导入", for: .normal) - contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) - contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular) - contactsBtn.rx.tap.subscribe(onNext: { [weak self] in - guard let self = self else { return } - self.phoneInputTF.resignFirstResponder() - let status = CNContactStore.authorizationStatus(for: .contacts) - switch status { - case .notDetermined: - CNContactStore().requestAccess(for: .contacts) { granted, _ in - DispatchQueue.main.async { if granted { self.presentContactPicker() } } - } - case .authorized: - self.presentContactPicker() - case .denied, .restricted: - DLToast.show(text: "请在设置中开启通讯录权限") - default: - break - } - }).disposed(by: disposeBag) - inputView.addSubview(contactsBtn) - contactsBtn.layoutChain.edgesVertical().width(60).right(12) + let prefixLab = UILabel() + prefixLab.text = "+86" + prefixLab.font = .systemFont(ofSize: 15, weight: .medium) + prefixLab.textColor = UIColor(hexStr: "#293445") + inputView.addSubview(prefixLab) + prefixLab.layoutChain + .left(16) + .centerY() + .width(40) inputView.addSubview(phoneInputTF) phoneInputTF.layoutChain .edgesVertical(10) - .left(15) - .rightToLeftOfView(contactsBtn, offset: -8) + .leftToRightOfView(prefixLab, offset: 0) + .right(16) let warningIcon = UIImageView(image: UIImage(named: "SearchLocation/warning")) warningIcon.contentMode = .scaleAspectFit view.addSubview(warningIcon) warningIcon.layoutChain - .topToBottomOfView(inputView, offset: 10) - .left(15) + .topToBottomOfView(inputView, offset: 12) + .left(20) .width(14) .height(14) let tipsLab = UILabel() - tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私" + tipsLab.text = "仅支持查看有共同圈子成员的位置" tipsLab.font = .systemFont(ofSize: 12, weight: .regular) tipsLab.textColor = UIColor(hexStr: "#999999") view.addSubview(tipsLab) tipsLab.layoutChain .leftToRightOfView(warningIcon, offset: 4) .centerY(warningIcon) - .right(15) + .right(16) view.addSubview(searchBtn) searchBtn.layoutChain - .topToBottomOfView(warningIcon, offset: 24) - .edgesHorzontal(20) - .height(50) - .bottom(24) + .topToBottomOfView(warningIcon, offset: 16) + .edgesHorzontal(16) + .height(56) + .bottom(20) return view }() lazy var phoneInputTF: UITextField = { let textField = UITextField() - textField.font = .systemFont(ofSize: 14, weight: .medium) - textField.placeholder = "输入要查看的号码" + textField.font = .systemFont(ofSize: 15, weight: .medium) + textField.placeholder = "输入要查看的手机号码或ID" textField.keyboardType = .numberPad textField.returnKeyType = .done return textField @@ -390,20 +363,4 @@ class SearchLocationView: UIView { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func presentContactPicker() { - let picker = CNContactPickerViewController() - picker.delegate = self - picker.displayedPropertyKeys = [CNContactPhoneNumbersKey] - UIViewController.topMost?.present(picker, animated: true) - } -} - -extension SearchLocationView: CNContactPickerDelegate { - func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) { - if let phone = contact.phoneNumbers.first?.value.stringValue { - phoneInputTF.text = phone.filter { $0.isNumber } - phoneInputTF.sendActions(for: .editingChanged) - } - } } diff --git a/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift b/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift index 0846056a..ef38b40b 100644 --- a/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift +++ b/QuickLocation/Section/Mine/MyProfile/AvatarIconListVC.swift @@ -57,7 +57,7 @@ class AvatarIconListVC: BaseViewController { let picker = PhotoPickerVC(maxCount: 1) picker.onConfirm = { [weak self] images in guard let image = images.first else { return } - self?.uploadAndChangeAvatar(image) + self?.presentCrop(for: image) } present(picker, animated: true) } @@ -70,13 +70,21 @@ class AvatarIconListVC: BaseViewController { let vc = CameraCaptureVC(mode: .photo) vc.onPhoto = { [weak self] image in self?.dismiss(animated: true) { - self?.uploadAndChangeAvatar(image) + self?.presentCrop(for: image) } } vc.modalPresentationStyle = .fullScreen present(vc, animated: true) } + private func presentCrop(for image: UIImage) { + let crop = ImageCropVC(image: image) + crop.onConfirm = { [weak self] cropped in + self?.uploadAndChangeAvatar(cropped) + } + present(crop, animated: true) + } + private func uploadAndChangeAvatar(_ image: UIImage) { guard let jpeg = image.jpegData(compressionQuality: 0.8) else { DLToast.show(text: "图片处理失败")