parent
9aa7b05066
commit
25aac678ac
|
|
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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>"; };
|
||||
|
|
@ -806,6 +809,7 @@
|
|||
AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; };
|
||||
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; };
|
||||
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = "<group>"; };
|
||||
C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageCropVC.swift; sourceTree = "<group>"; };
|
||||
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = "<group>"; };
|
||||
D64156B4A0E806901AB223FA /* DeviceActivityMonitorExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DeviceActivityMonitorExtension.swift; path = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
|
@ -817,21 +821,29 @@
|
|||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
30CCDF8E2FE3E63B00F5214A /* sound */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = sound;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30CCDF902FE3E63B00F5214A /* video */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = video;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30CCE01E2FE3E64700F5214A /* lotties */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = lotties;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30D87CEF2FDFF52100E958FD /* TTGTagCollectionView */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = TTGTagCollectionView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -991,6 +1003,7 @@
|
|||
305A74DB2FCA8C7000227D26 /* CornerRadiusView */,
|
||||
305A74E22FCA8C7000227D26 /* ImagePicker */,
|
||||
305A74E62FCA8C7000227D26 /* TextTableViewCell */,
|
||||
B2DDB0083141D63A991C9FEE /* ImageCrop */,
|
||||
);
|
||||
path = Component;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -2110,6 +2123,16 @@
|
|||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B2DDB0083141D63A991C9FEE /* ImageCrop */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51E818049FA5A8557C434E37 /* ImageCropView.swift */,
|
||||
C39ADD47D3C81F01F6289AC4 /* ImageCropVC.swift */,
|
||||
);
|
||||
name = ImageCrop;
|
||||
path = ImageCrop;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
22
QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json
vendored
Normal file
22
QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/Contents.json
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
BIN
QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png
vendored
Normal file
BIN
QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@3x.png
vendored
Normal file
BIN
QuickLocation/Assets.xcassets/SearchLocation/area_badge.imageset/area_badge@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>()
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: "图片处理失败")
|
||||
|
|
|
|||
Loading…
Reference in New Issue