Commit f78b7dc5 authored by yqz's avatar yqz

Merge branch 'develop_0409' into yQz0507

* develop_0409:
  【优化】部分代码优化
  【优化】1、优化流程,修改
  【新增】联系人主要功能逻辑代码
  【新增】添加重复联系人点击逻辑
  【修改】修改压缩方法
parents cd9820bc 77da9609
...@@ -21,7 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -21,7 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
window = UIWindow(frame: UIScreen.main.bounds) window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = UIColor.colorWithHex(hexStr: launchColor) window?.backgroundColor = .white
window?.overrideUserInterfaceStyle = .light window?.overrideUserInterfaceStyle = .light
let Ssoryboard = UIStoryboard(name: "LauchVC", bundle: nil) let Ssoryboard = UIStoryboard(name: "LauchVC", bundle: nil)
...@@ -51,6 +51,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -51,6 +51,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidBecomeActive(_ application: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
NotificationCenter.default.post(name: Notification.Name("applicationDidBecomeActive"), object: nil) NotificationCenter.default.post(name: Notification.Name("applicationDidBecomeActive"), object: nil)
postContactNotification()
}
func postContactNotification(){
// 通知联系人重复项改变
let dataUpdated = Notification.Name(ContactDupPreNormalView.CONTACT_MERGED)
NotificationCenter.default.post(name: dataUpdated, object: nil, userInfo: nil)
// 发起通知刷新重复联系人预览页面数据
let dataUpdatePre = Notification.Name(ContactDupPreNormalView.CONTACT_MERGED_PRE)
NotificationCenter.default.post(name: dataUpdatePre, object: nil, userInfo: nil)
// 发起通知刷新所有联系人页面数据
let dataUpdateAll = Notification.Name(ContactAllView.CONTACT_ALL)
NotificationCenter.default.post(name: dataUpdateAll, object: nil, userInfo: nil)
// 发起通知刷新不完整联系人页面数据
let dataUpdateInc = Notification.Name(ContactNormalIncomView.CONTACT_INCOM)
NotificationCenter.default.post(name: dataUpdateInc, object: nil, userInfo: nil)
} }
......
...@@ -258,7 +258,7 @@ class CompressQualityController : BaseViewController{ ...@@ -258,7 +258,7 @@ class CompressQualityController : BaseViewController{
}else{ }else{
// 压缩视频 // 压缩视频
var compressAllSize : Double = 0.0 var compressAllSize : Double = 0.0
CompressViewModel.compressVideos(models: self.model!, quality: Float(currentQulity)) { (identifier, progress) in CompressViewModel.compressVideos(models: self.model!, quality: Float(currentQulity)) { progress in
compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1) compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1)
} completion: { (outputURLs, errors) in } completion: { (outputURLs, errors) in
for (index, outputURL) in outputURLs.enumerated() { for (index, outputURL) in outputURLs.enumerated() {
...@@ -268,17 +268,16 @@ class CompressQualityController : BaseViewController{ ...@@ -268,17 +268,16 @@ class CompressQualityController : BaseViewController{
if let fileSize = attributes[.size] as? Int64 { if let fileSize = attributes[.size] as? Int64 {
compressAllSize = compressAllSize + Double(fileSize) compressAllSize = compressAllSize + Double(fileSize)
} }
Print("---------压缩后的大小:\(compressAllSize)")
} catch { } catch {
Print("获取视频文件大小失败") Print("获取视频文件大小失败")
} }
print("Compressed video \(index) saved at: \(outputURL)")
} else if let error = errors[index] { } else if let error = errors[index] {
print("Error compressing video \(index): \(error.localizedDescription)") print("Error compressing video \(index): \(error.localizedDescription)")
} }
// 不管成功失败都跳转下一页
self.updateNextView(compressAllSize,compressingView,[],outputURLs)
} }
Print("---------压缩后的大小:\(compressAllSize)")
// 不管成功失败都跳转下一页
self.updateNextView(compressAllSize,compressingView,[],outputURLs)
} }
} }
......
...@@ -153,160 +153,15 @@ class CompressViewModel{ ...@@ -153,160 +153,15 @@ class CompressViewModel{
/// - quality: 压缩质量 /// - quality: 压缩质量
/// - progress: 进度回调 /// - progress: 进度回调
/// - completion: 完成回调 /// - completion: 完成回调
static func compressVideos(models: [AssetModel], quality: Float, static func compressVideos(
progress: @escaping (String, Float) -> Void, models: [AssetModel],
completion: @escaping ([URL?], [Error?]) -> Void) { quality: Float,
progress: @escaping (Float) -> Void,
var outputURLs = [URL?](repeating: nil, count: models.count) completion: @escaping ([URL?], [Error?]) -> Void
var errors = [Error?](repeating: nil, count: models.count) ) {
let group = DispatchGroup() VideoCompressor.compressVideos(models: models, quality: quality, progress: progress, completion: completion)
for (index, model) in models.enumerated() {
group.enter()
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [model.localIdentifier], options: nil)
guard let asset = fetchResult.firstObject else {
errors[index] = NSError(domain: "VideoCompression", code: 404, userInfo: [NSLocalizedDescriptionKey: "Asset not found"])
group.leave()
continue
}
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { (avAsset, _, _) in
guard let avAsset = avAsset else {
errors[index] = NSError(domain: "VideoCompression", code: 500, userInfo: [NSLocalizedDescriptionKey: "Could not load asset"])
group.leave()
return
}
// 获取原始视频信息
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
errors[index] = NSError(domain: "VideoCompression", code: 501, userInfo: [NSLocalizedDescriptionKey: "No video track found"])
group.leave()
return
}
let originalBitrate = videoTrack.estimatedDataRate
let originalSize = model.assetSize
Print("---------原始大小:\(originalSize)")
// 压缩设置
let targetBitrate: Float
if originalBitrate > 0 {
// 强制压缩到原始比特率的quality比例,最低不低于500kbps
if(originalSize <= 100000){
// 当大小已经没有100KB时,按照0.95去压缩
targetBitrate = min(originalBitrate * quality, originalBitrate * 0.95)
}else{
// 最低500kbps
targetBitrate = max(originalBitrate * quality, 500_000)
}
} else {
// 无法获取原始比特率时的默认值
// 1Mbps为基准
targetBitrate = quality * 1_000_000
}
// 创建输出URL
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
// 压缩参数
let compressionProperties: [String: Any] = [
AVVideoAverageBitRateKey: targetBitrate,
// 关键帧间隔加大到4秒(30fps)
AVVideoMaxKeyFrameIntervalKey: 120,
// 使用基线配置
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline30,
// 禁用B帧
AVVideoAllowFrameReorderingKey: false,
// 降低帧率到15fps
AVVideoExpectedSourceFrameRateKey: 15
]
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
// 分辨率减半
AVVideoWidthKey: videoTrack.naturalSize.width * 2 / 3,
AVVideoHeightKey: videoTrack.naturalSize.height * 2 / 3,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: compressionProperties
]
// 音频设置也进行压缩
let audioSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
// 单声道
AVNumberOfChannelsKey: 1,
// 降低采样率
AVSampleRateKey: 22050,
// 64kbps音频比特率
AVEncoderBitRateKey: 64_000
]
// 创建导出会话
guard let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetLowQuality) else {
errors[index] = NSError(domain: "VideoCompression", code: 502, userInfo: [NSLocalizedDescriptionKey: "Could not create export session"])
group.leave()
return
}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
exportSession.videoComposition = self.aggressiveVideoComposition(for: avAsset, settings: videoSettings)
exportSession.audioMix = self.aggressiveAudioMix(for: avAsset, settings: audioSettings)
exportSession.exportAsynchronously {
defer { group.leave() }
guard exportSession.status == .completed else {
errors[index] = exportSession.error ?? NSError(domain: "VideoCompression", code: 503, userInfo: [NSLocalizedDescriptionKey: "Export failed"])
try? FileManager.default.removeItem(at: outputURL)
return
}
// 强制验证文件大小
if let attributes = try? FileManager.default.attributesOfItem(atPath: outputURL.path),
let compressedSize = attributes[.size] as? NSNumber {
if compressedSize.doubleValue >= originalSize {
// 如果压缩后大于等于原始大小,使用更激进的设置重新压缩
try? FileManager.default.removeItem(at: outputURL)
self.recompressWithMoreAggressiveSettings(avAsset: avAsset, originalSize: originalSize, index: index) { url, error in
outputURLs[index] = url
errors[index] = error
}
} else {
outputURLs[index] = outputURL
}
} else {
outputURLs[index] = outputURL
}
}
// 进度监控
DispatchQueue.global().async {
while exportSession.status == .waiting || exportSession.status == .exporting {
DispatchQueue.main.async {
progress(model.localIdentifier, exportSession.progress)
}
Thread.sleep(forTimeInterval: 0.1)
}
}
}
}
group.notify(queue: .main) {
completion(outputURLs, errors)
}
} }
/// 压缩多张图片 /// 压缩多张图片
/// - Parameters: /// - Parameters:
/// - assets: 图片集合 /// - assets: 图片集合
...@@ -409,97 +264,5 @@ class CompressViewModel{ ...@@ -409,97 +264,5 @@ class CompressViewModel{
} }
} }
private static func recompressWithMoreAggressiveSettings(avAsset: AVAsset, originalSize: Double, index: Int, completion: @escaping (URL?, Error?) -> Void) {
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
completion(nil, NSError(domain: "VideoCompression", code: 504, userInfo: [NSLocalizedDescriptionKey: "No video track found"]))
return
}
// 更激进的设置
let compressionProperties: [String: Any] = [
// 固定250kbps
AVVideoAverageBitRateKey: 250_000,
// 关键帧间隔8秒
AVVideoMaxKeyFrameIntervalKey: 240,
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline30,
AVVideoAllowFrameReorderingKey: false,
// 帧率降到10fps
AVVideoExpectedSourceFrameRateKey: 10
]
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: videoTrack.naturalSize.width / 2,
AVVideoHeightKey: videoTrack.naturalSize.height / 2,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: compressionProperties
]
let audioSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: 1,
// 16kHz采样率
AVSampleRateKey: 16000,
// 32kbps音频比特率
AVEncoderBitRateKey: 32_000
]
guard let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetLowQuality) else {
completion(nil, NSError(domain: "VideoCompression", code: 505, userInfo: [NSLocalizedDescriptionKey: "Could not create export session"]))
return
}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
exportSession.videoComposition = self.aggressiveVideoComposition(for: avAsset, settings: videoSettings)
exportSession.audioMix = self.aggressiveAudioMix(for: avAsset, settings: audioSettings)
exportSession.exportAsynchronously {
if exportSession.status == .completed {
completion(outputURL, nil)
} else {
try? FileManager.default.removeItem(at: outputURL)
completion(nil, exportSession.error ?? NSError(domain: "VideoCompression", code: 506, userInfo: [NSLocalizedDescriptionKey: "Recompression failed"]))
}
}
}
private static func aggressiveVideoComposition(for asset: AVAsset, settings: [String: Any]) -> AVVideoComposition? {
guard let videoTrack = asset.tracks(withMediaType: .video).first else { return nil }
let composition = AVMutableVideoComposition()
composition.renderSize = CGSize(
width: (settings[AVVideoWidthKey] as? CGFloat) ?? videoTrack.naturalSize.width,
height: (settings[AVVideoHeightKey] as? CGFloat) ?? videoTrack.naturalSize.height
)
composition.frameDuration = CMTime(value: 1, timescale: Int32(settings[AVVideoExpectedSourceFrameRateKey] as? Int ?? 15))
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
instruction.layerInstructions = [layerInstruction]
composition.instructions = [instruction]
return composition
}
private static func aggressiveAudioMix(for asset: AVAsset, settings: [String: Any]) -> AVAudioMix? {
guard let audioTrack = asset.tracks(withMediaType: .audio).first else { return nil }
let audioMix = AVMutableAudioMix()
let audioInputParams = AVMutableAudioMixInputParameters(track: audioTrack)
// 保持音频处理简单
audioInputParams.audioTimePitchAlgorithm = .timeDomain
audioMix.inputParameters = [audioInputParams]
return audioMix
}
} }
@preconcurrency import AVFoundation
import Photos
class VideoCompressor {
private static let compressionQueue = DispatchQueue(label: "com.compression.queue", qos: .userInitiated)
static func compressVideos(
models: [AssetModel],
quality: Float,
progress: @escaping (Float) -> Void,
completion: @escaping ([URL?], [Error?]) -> Void
) {
var results = [URL?](repeating: nil, count: models.count)
var errors = [Error?](repeating: nil, count: models.count)
let group = DispatchGroup()
var totalProgress: Float = 0
let totalVideos = Float(models.count)
var individualProgress = [Float](repeating: 0, count: models.count)
for (index, model) in models.enumerated() {
group.enter()
fetchAVAsset(for: model) { asset, error in
guard let asset = asset else {
errors[index] = error ?? NSError(domain: "AssetError", code: -1, userInfo: nil)
group.leave()
return
}
// 这里再添加一个逻辑 当原大小小于等于为100时压缩比例调成0.98
var tempScale = quality
if model.assetSize <= 102400 {
tempScale = 0.98
}
compressionQueue.async {
compressSingleVideo(
asset: asset,
quality: tempScale,
progress: { p in
DispatchQueue.main.async {
// 更新单个视频的进度
individualProgress[index] = p
// 重新计算总进度
totalProgress = individualProgress.reduce(0, +) / totalVideos
progress(totalProgress)
}
},
completion: { url, error in
results[index] = url
errors[index] = error
group.leave()
}
)
}
}
}
group.notify(queue: .main) {
completion(results, errors)
}
}
// MARK: - 核心压缩方法(闭包版本)
private static func compressSingleVideo(
asset: AVAsset,
quality: Float,
progress: @escaping (Float) -> Void,
completion: @escaping (URL?, Error?) -> Void
) {
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
// 清理已存在的文件
try? FileManager.default.removeItem(at: outputURL)
// 异步加载视频轨道
loadTracksAsync(asset: asset) { videoTrack, audioTrack, error in
guard let videoTrack = videoTrack else {
completion(nil, error ?? CompressionError.invalidVideoTrack)
return
}
do {
let (reader, videoOutput, writer, videoInput) = try setupVideoComponents(
videoTrack: videoTrack,
outputURL: outputURL,
quality: quality
)
let (audioOutput, audioInput) = try setupAudioComponents(audioTrack: audioTrack)
// 配置读写器
if let audioOutput = audioOutput, let audioInput = audioInput {
reader.add(audioOutput)
writer.add(audioInput)
}
// 开始处理
processSamples(
reader: reader,
writer: writer,
videoOutput: videoOutput,
videoInput: videoInput,
audioOutput: audioOutput,
audioInput: audioInput,
duration: CMTimeGetSeconds(asset.duration),
progress: progress,
completion: { result in
switch result {
case .success:
completion(outputURL, nil)
case .failure(let error):
completion(nil, error)
}
}
)
} catch {
completion(nil, error)
}
}
}
// MARK: - 异步加载轨道(兼容iOS 14)
private static func loadTracksAsync(
asset: AVAsset,
completion: @escaping (AVAssetTrack?, AVAssetTrack?, Error?) -> Void
) {
let keys: [String] = [
#keyPath(AVAsset.tracks),
#keyPath(AVAsset.duration)
]
asset.loadValuesAsynchronously(forKeys: keys) {
var error: NSError?
let status = asset.statusOfValue(forKey: #keyPath(AVAsset.tracks), error: &error)
guard status == .loaded else {
completion(nil, nil, error ?? CompressionError.trackLoadingFailed)
return
}
let videoTrack = asset.tracks(withMediaType: .video).first
let audioTrack = asset.tracks(withMediaType: .audio).first
completion(videoTrack, audioTrack, nil)
}
}
// MARK: - 视频组件配置
private static func setupVideoComponents(
videoTrack: AVAssetTrack,
outputURL: URL,
quality: Float
) throws -> (AVAssetReader, AVAssetReaderTrackOutput, AVAssetWriter, AVAssetWriterInput) {
// 视频读取配置
let readerOutputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
]
guard let reader = try? AVAssetReader(asset: videoTrack.asset!) else {
throw CompressionError.readerInitializationFailed
}
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: readerOutputSettings)
guard reader.canAdd(videoOutput) else {
throw CompressionError.readerInitializationFailed
}
reader.add(videoOutput)
// 视频写入配置
guard let writer = try? AVAssetWriter(outputURL: outputURL, fileType: .mp4) else {
throw CompressionError.writerInitializationFailed
}
let originalSize = videoTrack.naturalSize
let originalBitrate = Double(videoTrack.estimatedDataRate)
let targetSize = calculateTargetSize(originalSize: originalSize, quality: quality)
let videoInput = AVAssetWriterInput(
mediaType: .video,
outputSettings: [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: targetSize.width,
AVVideoHeightKey: targetSize.height,
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: quality * Float(originalBitrate),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
AVVideoMaxKeyFrameIntervalKey: 30
]
]
)
videoInput.transform = videoTrack.preferredTransform
guard writer.canAdd(videoInput) else {
throw CompressionError.writerInitializationFailed
}
writer.add(videoInput)
return (reader, videoOutput, writer, videoInput)
}
// MARK: - 音频组件配置
private static func setupAudioComponents(
audioTrack: AVAssetTrack?
) throws -> (AVAssetReaderTrackOutput?, AVAssetWriterInput?) {
guard let audioTrack = audioTrack else {
return (nil, nil)
}
// 音频读取配置
let audioOutput = AVAssetReaderTrackOutput(
track: audioTrack,
outputSettings: [
AVFormatIDKey: kAudioFormatLinearPCM
]
)
// 音频写入配置
let audioInput = AVAssetWriterInput(
mediaType: .audio,
outputSettings: [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100,
AVEncoderBitRateKey: 128000,
AVNumberOfChannelsKey: 2
]
)
return (audioOutput, audioInput)
}
// MARK: - 处理样本数据
// 修改后的 processSamples 方法
private static func processSamples(
reader: AVAssetReader,
writer: AVAssetWriter,
videoOutput: AVAssetReaderTrackOutput,
videoInput: AVAssetWriterInput,
audioOutput: AVAssetReaderTrackOutput?,
audioInput: AVAssetWriterInput?,
duration: Float64,
progress: @escaping (Float) -> Void,
completion: @escaping (Result<Void, Error>) -> Void
) {
let videoQueue = DispatchQueue(label: "video.queue")
let audioQueue = DispatchQueue(label: "audio.queue")
let stateQueue = DispatchQueue(label: "state.queue")
// 使用线程安全的状态管理
var _videoFinished = false
var _audioFinished = false
func updateVideoFinished(_ value: Bool) {
stateQueue.async {
_videoFinished = value
checkCompletion()
}
}
func updateAudioFinished(_ value: Bool) {
stateQueue.async {
_audioFinished = value
checkCompletion()
}
}
func checkCompletion() {
stateQueue.async {
guard _videoFinished && _audioFinished else { return }
writer.finishWriting {
// 切换到主队列处理完成回调
DispatchQueue.main.async {
switch writer.status {
case .completed:
completion(.success(()))
case .failed, .cancelled:
completion(.failure(writer.error ?? CompressionError.exportFailed))
default:
completion(.failure(CompressionError.exportFailed))
}
}
}
}
}
writer.startWriting()
reader.startReading()
writer.startSession(atSourceTime: .zero)
// 处理视频样本
videoInput.requestMediaDataWhenReady(on: videoQueue) {
while videoInput.isReadyForMoreMediaData {
guard reader.status == .reading,
let sampleBuffer = videoOutput.copyNextSampleBuffer() else {
videoInput.markAsFinished()
updateVideoFinished(true)
break
}
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let currentProgress = Float(time.seconds / duration)
DispatchQueue.main.async {
progress(currentProgress)
}
videoInput.append(sampleBuffer)
}
}
// 处理音频样本
if let audioInput = audioInput, let audioOutput = audioOutput {
audioInput.requestMediaDataWhenReady(on: audioQueue) {
while audioInput.isReadyForMoreMediaData {
guard let sampleBuffer = audioOutput.copyNextSampleBuffer() else {
audioInput.markAsFinished()
updateAudioFinished(true)
break
}
audioInput.append(sampleBuffer)
}
}
} else {
updateAudioFinished(true)
}
}
// MARK: - 其他工具方法
private static func calculateTargetSize(originalSize: CGSize, quality: Float) -> CGSize {
let scale = max(0.1, min(1.0, quality))
return CGSize(
width: round(originalSize.width * CGFloat(scale)),
height: round(originalSize.height * CGFloat(scale))
)
}
private static func fetchAVAsset(
for model: AssetModel,
completion: @escaping (AVAsset?, Error?) -> Void
) {
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [model.localIdentifier], options: nil)
guard let phAsset = fetchResult.firstObject else {
completion(nil, CompressionError.assetNotFound)
return
}
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
PHImageManager.default().requestAVAsset(
forVideo: phAsset,
options: options
) { asset, _, info in
completion(asset, info?[PHImageErrorKey] as? Error)
}
}
enum CompressionError: Error {
case readerInitializationFailed
case writerInitializationFailed
case invalidVideoTrack
case trackLoadingFailed
case exportFailed
case assetNotFound
}
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import Contacts
class ContactAllViewController : BaseViewController { class ContactAllViewController : BaseViewController {
...@@ -63,6 +64,14 @@ class ContactAllViewController : BaseViewController { ...@@ -63,6 +64,14 @@ class ContactAllViewController : BaseViewController {
make.top.left.right.equalToSuperview() make.top.left.right.equalToSuperview()
make.height.equalTo(statusBarHeight + 44) make.height.equalTo(statusBarHeight + 44)
} }
self.normalView.dataClearCallBack = {
self.setDefaultPage()
}
// 发起通知刷新重复联系人预览页面数据
let dataUpdateAll = Notification.Name(ContactAllView.CONTACT_ALL)
NotificationCenter.default.addObserver(self, selector: #selector(handleDataUpdated(_:)), name: dataUpdateAll, object: nil)
} }
} }
...@@ -90,4 +99,40 @@ extension ContactAllViewController { ...@@ -90,4 +99,40 @@ extension ContactAllViewController {
} }
} }
@objc func handleDataUpdated(_ notification: Notification) {
let store = CNContactStore()
let keysToFetch = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
do {
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
var allContacts : [ContactModel] = []
try store.enumerateContacts(with: request) { contact, stop in
if let model = ContactModel.init(contact: contact) {
allContacts.append(model)
}
}
DispatchQueue.main.async {
self.normalView.dataSourceModel = allContacts
if allContacts.count > 0 {
self.setNormalPage()
self.normalView.subTitleLabel.text = "\(allContacts.count) Contacts"
self.normalView.sortContacts()
self.normalView.tableView.reloadData()
self.normalView.setupCustomIndexView()
}else{
self.setDefaultPage()
}
}
} catch {
DispatchQueue.main.async {
print("获取全部联系人信息时发生错误: \(error)")
}
}
}
} }
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import Contacts
class ContactBackupDetailViewController : BaseViewController { class ContactBackupDetailViewController : BaseViewController {
...@@ -137,7 +138,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -137,7 +138,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
let sectionTitle = sectionTitles[indexPath.section] let sectionTitle = sectionTitles[indexPath.section]
let contact = sectionedContacts[sectionTitle]?[indexPath.row] let contact = sectionedContacts[sectionTitle]?[indexPath.row]
cell.model = contact cell.model = contact
cell.nameLabel.text = contact?.name cell.nameLabel.text = contact?.fullName
return cell return cell
} }
...@@ -157,6 +158,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -157,6 +158,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
return 20 return 20
} }
func setupCustomIndexView() { func setupCustomIndexView() {
customIndexView = nil
customIndexView = UIStackView() customIndexView = UIStackView()
customIndexView.axis = .vertical customIndexView.axis = .vertical
customIndexView.alignment = .center customIndexView.alignment = .center
...@@ -166,9 +168,9 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -166,9 +168,9 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
self.customIndexView.snp.makeConstraints { make in self.customIndexView.snp.makeConstraints { make in
make.left.equalTo(self.tableView.snp.right).offset(0) make.left.equalTo(self.tableView.snp.right).offset(0)
make.top.equalTo(self.subTitleLabel.snp.bottom).offset(77) make.centerY.equalTo(self.view.snp.centerY)
make.width.equalTo(15 * RScreenW()) make.width.equalTo(15 * RScreenW())
make.height.equalTo(354) make.height.equalTo(self.sectionTitles.count * (14 + 2))
} }
for (index, section) in sectionTitles.enumerated() { for (index, section) in sectionTitles.enumerated() {
...@@ -193,8 +195,17 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -193,8 +195,17 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
} }
} }
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleRect = CGRect(origin: tableView.contentOffset, size: tableView.bounds.size) // 获取可见区域的中心点,稍微向下偏移以更准确地检测当前可见的 section
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.minY) let visibleRect = CGRect(
origin: tableView.contentOffset,
size: tableView.bounds.size
)
// 将检测点从顶部边缘下移一些,例如下移10%的可见区域高度
let adjustedY = visibleRect.minY + visibleRect.height * 0.1
let visiblePoint = CGPoint(x: visibleRect.midX, y: adjustedY)
// 获取可见区域最顶部的单元格的 indexPath
if let visibleIndexPath = tableView.indexPathForRow(at: visiblePoint) { if let visibleIndexPath = tableView.indexPathForRow(at: visiblePoint) {
let newIndex = visibleIndexPath.section let newIndex = visibleIndexPath.section
if newIndex != selectedIndex { if newIndex != selectedIndex {
...@@ -218,7 +229,6 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -218,7 +229,6 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
let indexPath = IndexPath(row: 0, section: index) let indexPath = IndexPath(row: 0, section: index)
tableView.scrollToRow(at: indexPath, at: .top, animated: true) tableView.scrollToRow(at: indexPath, at: .top, animated: true)
selectedIndex = index selectedIndex = index
updateIndexStyles()
} }
} }
...@@ -227,7 +237,21 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -227,7 +237,21 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
let alertVc = ContactBacRestoreAlertView(frame: self.view.bounds) let alertVc = ContactBacRestoreAlertView(frame: self.view.bounds)
self.view.addSubview(alertVc) self.view.addSubview(alertVc)
alertVc.sureCallBack = { alertVc.sureCallBack = {
ContactManager.performAtomicRestore(self.dataSourceModel) { success, error in
if success {
print("恢复成功!")
DispatchQueue.main.async {
// 再次请求数据 重新刷新页面
let buAlertVc = ContactRestoreSuccessView(frame: self.view.bounds)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
buAlertVc.removeFromSuperview()
}
}
} else {
print("失败原因: \(error?.localizedDescription ?? "未知错误")")
}
}
} }
} }
...@@ -236,7 +260,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -236,7 +260,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
func sortContacts() { func sortContacts() {
sectionedContacts.removeAll() sectionedContacts.removeAll()
for contact in self.dataSourceModel { for contact in self.dataSourceModel {
let firstLetter = pinyinFirstLetter(contact.name).uppercased() let firstLetter = pinyinFirstLetter(contact.fullName).uppercased()
if sectionedContacts[firstLetter] == nil { if sectionedContacts[firstLetter] == nil {
sectionedContacts[firstLetter] = [] sectionedContacts[firstLetter] = []
} }
...@@ -246,7 +270,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS ...@@ -246,7 +270,7 @@ extension ContactBackupDetailViewController:UITableViewDelegate,UITableViewDataS
sectionTitles = sectionedContacts.keys.sorted() sectionTitles = sectionedContacts.keys.sorted()
for key in sectionTitles { for key in sectionTitles {
sectionedContacts[key] = sectionedContacts[key]?.sorted { sectionedContacts[key] = sectionedContacts[key]?.sorted {
return pinyinFirstLetter($0.name).uppercased() < pinyinFirstLetter($1.name).uppercased() return pinyinFirstLetter($0.fullName).uppercased() < pinyinFirstLetter($1.fullName).uppercased()
} }
} }
} }
......
...@@ -33,6 +33,7 @@ class ContactBackupViewController : BaseViewController { ...@@ -33,6 +33,7 @@ class ContactBackupViewController : BaseViewController {
// 默认页面 // 默认页面
func setDefaultPage(){ func setDefaultPage(){
self.normalView.removeFromSuperview()
self.view.addSubview(self.emptyView) self.view.addSubview(self.emptyView)
self.emptyView.snp.makeConstraints { make in self.emptyView.snp.makeConstraints { make in
...@@ -74,23 +75,37 @@ class ContactBackupViewController : BaseViewController { ...@@ -74,23 +75,37 @@ class ContactBackupViewController : BaseViewController {
// 备份之前先看看是否有可用的联系人 // 备份之前先看看是否有可用的联系人
if let data = self.dataSourceAllModel { if let data = self.dataSourceAllModel {
vm.backupAllContacts(data.allContacts) { finished, error in if data.allContacts.count > 0 {
if let error = error { vm.backupAllContacts(data.allContacts) { finished, error in
Print("添加失败,\(error.localizedDescription)") if let error = error {
} Print("添加失败,\(error.localizedDescription)")
DispatchQueue.main.async { }
// 再次请求数据 重新刷新页面 DispatchQueue.main.async {
let buAlertVc = ContactBackUpCompletedAlertView(frame: self.view.bounds) // 再次请求数据 重新刷新页面
self.view.addSubview(buAlertVc) let buAlertVc = ContactBackUpCompletedAlertView(frame: self.view.bounds)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.view.addSubview(buAlertVc)
buAlertVc.removeFromSuperview() DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.updateCurrentPageData() buAlertVc.removeFromSuperview()
self.updateCurrentPageData()
}
} }
} }
}else {
let buAlertVc = ContactBackUpNoDataAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
buAlertVc.removeFromSuperview()
}
} }
} }
} }
self.normalView.isClearAllCallBack = {
DispatchQueue.main.async {
self.updateCurrentPageData()
}
}
} }
} }
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import Foundation import Foundation
import SnapKit import SnapKit
import Contacts
class ContactIncompleteViewController : BaseViewController { class ContactIncompleteViewController : BaseViewController {
...@@ -39,6 +40,8 @@ class ContactIncompleteViewController : BaseViewController { ...@@ -39,6 +40,8 @@ class ContactIncompleteViewController : BaseViewController {
// 默认页面 // 默认页面
func setDefaultPage(){ func setDefaultPage(){
self.normalView.removeFromSuperview()
self.selectAllButton.isHidden = true
self.view.addSubview(self.emptyView) self.view.addSubview(self.emptyView)
self.emptyView.snp.makeConstraints { make in self.emptyView.snp.makeConstraints { make in
...@@ -49,6 +52,7 @@ class ContactIncompleteViewController : BaseViewController { ...@@ -49,6 +52,7 @@ class ContactIncompleteViewController : BaseViewController {
} }
func setNormalPage(){ func setNormalPage(){
self.emptyView.removeFromSuperview() self.emptyView.removeFromSuperview()
self.selectAllButton.isHidden = false
self.view.addSubview(self.normalView) self.view.addSubview(self.normalView)
self.normalView.snp.makeConstraints { make in self.normalView.snp.makeConstraints { make in
...@@ -94,7 +98,24 @@ class ContactIncompleteViewController : BaseViewController { ...@@ -94,7 +98,24 @@ class ContactIncompleteViewController : BaseViewController {
} }
self.normalView.tableView.reloadData() self.normalView.tableView.reloadData()
} }
self.normalView.dataClearCallBack = {
self.setDefaultPage()
}
self.normalView.selectDataChangeCallBack = {[weak self]show in
guard let self else {return}
if show {
self.selectAllButton.isSelected = false
}else {
self.selectAllButton.isSelected = true
}
}
self.setDefaultPage() self.setDefaultPage()
// 发起通知刷新重复联系人预览页面数据
let dataUpdateInc = Notification.Name(ContactNormalIncomView.CONTACT_INCOM)
NotificationCenter.default.addObserver(self, selector: #selector(handleDataUpdated(_:)), name: dataUpdateInc, object: nil)
} }
} }
...@@ -106,7 +127,6 @@ extension ContactIncompleteViewController { ...@@ -106,7 +127,6 @@ extension ContactIncompleteViewController {
self.setNormalPage() self.setNormalPage()
self.normalView.dataSourceModel = data self.normalView.dataSourceModel = data
DispatchQueue.main.async { DispatchQueue.main.async {
self.normalView.sortContacts()
self.normalView.subTitleLabel.text = "\(data.count) Contacts" self.normalView.subTitleLabel.text = "\(data.count) Contacts"
self.normalView.tableView.reloadData() self.normalView.tableView.reloadData()
} }
...@@ -118,4 +138,42 @@ extension ContactIncompleteViewController { ...@@ -118,4 +138,42 @@ extension ContactIncompleteViewController {
self.setDefaultPage() self.setDefaultPage()
} }
} }
@objc func handleDataUpdated(_ notification: Notification) {
let store = CNContactStore()
let keysToFetch = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
do {
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
// 创建数组
var incompleteContacts : [ContactModel] = []
try store.enumerateContacts(with: request) { contact, stop in
if let model = ContactModel.init(contact: contact){
if model.fullName.isEmpty || model.phoneNumber?.count ?? 0 <= 0 {
incompleteContacts.append(model)
}
}
}
DispatchQueue.main.async {
self.normalView.dataSourceModel = incompleteContacts
if incompleteContacts.count > 0 {
self.setNormalPage()
self.normalView.subTitleLabel.text = "\(incompleteContacts.count) Contacts"
self.normalView.tableView.reloadData()
}else{
self.setDefaultPage()
}
}
} catch {
DispatchQueue.main.async {
print("获取不完整联系人信息时发生错误: \(error)")
}
}
}
} }
...@@ -41,6 +41,7 @@ class ContactViewController : BaseViewController { ...@@ -41,6 +41,7 @@ class ContactViewController : BaseViewController {
} }
func setDefaultPage(){ func setDefaultPage(){
self.moduleView.removeFromSuperview()
self.view.addSubview(self.emptyView) self.view.addSubview(self.emptyView)
self.emptyView.snp.makeConstraints { make in self.emptyView.snp.makeConstraints { make in
make.top.equalTo(self.navView.snp.bottom).offset(0) make.top.equalTo(self.navView.snp.bottom).offset(0)
...@@ -117,26 +118,22 @@ extension ContactViewController{ ...@@ -117,26 +118,22 @@ extension ContactViewController{
var contactsByName: [String: [ContactModel]] = [:] var contactsByName: [String: [ContactModel]] = [:]
try store.enumerateContacts(with: request) { contact, stop in try store.enumerateContacts(with: request) { contact, stop in
let givenName = contact.givenName if let model = ContactModel.init(contact: contact) {
let familyName = contact.familyName
let fullName = "\(familyName)\(givenName)" if model.fullName.isEmpty || model.phoneNumber?.count ?? 0 <= 0 {
let phoneNumbers = contact.phoneNumbers.map { $0.value.stringValue } incompleteContacts.append(model)
let model = ContactModel.init(name: fullName, phoneNumber: phoneNumbers,identifier: contact.identifier) }
if fullName.isEmpty || phoneNumbers.count <= 0 { allContacts.append(model)
incompleteContacts.append(model) if !model.fullName.isEmpty {
} if contactsByName[model.fullName] == nil {
allContacts.append(model) contactsByName[model.fullName] = [model]
} else {
contactsByName[model.fullName]?.append(model)
if !fullName.isEmpty { }
if contactsByName[fullName] == nil {
contactsByName[fullName] = [model]
} else {
contactsByName[fullName]?.append(model)
} }
duplicates = contactsByName.values.filter { $0.count > 1 }
} }
duplicates = contactsByName.values.filter { $0.count > 1 }
} }
self.dataSourceModel = ContactModuleModel.init(duplicates: sortDupDataSource(orgData: duplicates), incompleteContacts: incompleteContacts, backups: [], allContacts: allContacts) self.dataSourceModel = ContactModuleModel.init(duplicates: sortDupDataSource(orgData: duplicates), incompleteContacts: incompleteContacts, backups: [], allContacts: allContacts)
DispatchQueue.main.async { DispatchQueue.main.async {
......
//
// ContactsDupPreViewController.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import Foundation
class ContactsDupPreViewController : BaseViewController {
var dataSourceModel : [String:[ContactModel]] = [:]
lazy var navView : ContactNavView = {
let view = ContactNavView()
return view
}()
lazy var emptyView:ContactNoDupPreView = {
let view = ContactNoDupPreView()
return view
}()
lazy var normalView : ContactDupPreNormalView = {
let view = ContactDupPreNormalView()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.navView)
self.navView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.height.equalTo(statusBarHeight + 44)
}
self.setDefaultPage()
self.normalView.dataChangeCallBack = {[weak self] isClear in
guard let self else {return}
if isClear {
DispatchQueue.main.async {
self.setDefaultPage()
}
}
}
let dataUpdatePre = Notification.Name(ContactDupPreNormalView.CONTACT_MERGED_PRE)
NotificationCenter.default.addObserver(self, selector: #selector(handleDataUpdated(_:)), name: dataUpdatePre, object: nil)
}
// 默认页面
func setDefaultPage(){
self.normalView.removeFromSuperview()
self.view.addSubview(self.emptyView)
self.emptyView.snp.makeConstraints { make in
make.top.equalTo(self.navView.snp.bottom).offset(0)
make.left.right.equalToSuperview()
make.bottom.equalToSuperview()
}
}
func setNormalPage(){
self.emptyView.removeFromSuperview()
self.view.addSubview(self.normalView)
self.normalView.snp.makeConstraints { make in
make.top.equalTo(self.navView.snp.bottom).offset(0)
make.left.right.equalToSuperview()
make.bottom.equalToSuperview()
}
}
}
extension ContactsDupPreViewController {
fileprivate func dealWhenSystemDataUpdate() {
// 这里还需要处理下万一当前用户在外部修改了下联系人
var changed : Bool = false
for (_,value) in self.dataSourceModel {
// 看名字和电话号码是否全部能对上
for contact in value {
// 从系统取出联系人
if let sysContact = ContactManager.fetchContactFromStore(model: contact){
let fullName = "\(sysContact.familyName)\(sysContact.givenName)"
let phoneNumbers = sysContact.phoneNumbers.map { $0.value.stringValue}
if let numbers = contact.phoneNumber {
// 两个相等
if fullName != contact.fullName || phoneNumbers.sorted() != numbers.sorted(){
changed = true
break
}
}else {
changed = true
break
}
}else {
changed = true
break
}
}
if changed == true {
break
}
}
// 如果变了
if changed == true {
self.dataSourceModel = [:]
setDefaultPage()
}else{
var finallyData : [[ContactModel]] = []
for (_,value) in self.dataSourceModel {
finallyData.append(value)
}
self.normalView.dataSourceModel = finallyData
DispatchQueue.main.async {
self.normalView.subTitleLabel.text = "\(self.dataSourceModel.count) Contacts"
var count : Int = 0
for item in finallyData {
count += item.count
}
self.normalView.mergeButtonView.mergeButton.setTitle("Merge \(count) Contacts", for: .normal)
self.normalView.tableView.reloadData()
}
}
}
fileprivate func dealDataToNoEmpty() {
var tempArray : [String:[ContactModel]] = [:]
for (key,value) in self.dataSourceModel {
if value.count > 0 {
tempArray[key] = value
}
}
self.dataSourceModel = tempArray
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
dealDataToNoEmpty()
if self.dataSourceModel.count > 0 {
self.setNormalPage()
var finallyData : [[ContactModel]] = []
for (_,value) in self.dataSourceModel {
finallyData.append(value)
}
self.normalView.dataSourceModel = finallyData
DispatchQueue.main.async {
self.normalView.subTitleLabel.text = "\(self.dataSourceModel.count) Contacts"
var count : Int = 0
for item in finallyData {
count += item.count
}
self.normalView.mergeButtonView.mergeButton.setTitle("Merge \(count) Contacts", for: .normal)
self.normalView.tableView.reloadData()
}
}else{
self.setDefaultPage()
}
}
@objc func handleDataUpdated(_ notification: Notification) {
dealWhenSystemDataUpdate()
}
}
...@@ -6,15 +6,26 @@ ...@@ -6,15 +6,26 @@
// //
import Foundation import Foundation
import SnapKit
import Contacts
class ContactsDupViewController : BaseViewController { class ContactsDupViewController : BaseViewController {
var dataSourceModel : [[ContactModel]]? var dataSourceModel : [[ContactModel]]?
private var widthConstraint: Constraint?
lazy var navView : ContactNavView = { lazy var navView : ContactNavView = {
let view = ContactNavView() let view = ContactNavView()
return view return view
}() }()
lazy var selectAllButton : SelectAllButton = {
let view = SelectAllButton()
view.clipsToBounds = true
view.layer.cornerRadius = 16
return view
}()
lazy var emptyView : ContactNoDupView = { lazy var emptyView : ContactNoDupView = {
let view = ContactNoDupView() let view = ContactNoDupView()
return view return view
...@@ -28,6 +39,7 @@ class ContactsDupViewController : BaseViewController { ...@@ -28,6 +39,7 @@ class ContactsDupViewController : BaseViewController {
// 默认页面 // 默认页面
func setDefaultPage(){ func setDefaultPage(){
self.normalView.removeFromSuperview() self.normalView.removeFromSuperview()
self.selectAllButton.isHidden = true
self.view.addSubview(self.emptyView) self.view.addSubview(self.emptyView)
self.emptyView.snp.makeConstraints { make in self.emptyView.snp.makeConstraints { make in
...@@ -40,7 +52,7 @@ class ContactsDupViewController : BaseViewController { ...@@ -40,7 +52,7 @@ class ContactsDupViewController : BaseViewController {
func setNormalPage(){ func setNormalPage(){
self.emptyView.removeFromSuperview() self.emptyView.removeFromSuperview()
self.view.addSubview(self.normalView) self.view.addSubview(self.normalView)
self.selectAllButton.isHidden = false
self.normalView.snp.makeConstraints { make in self.normalView.snp.makeConstraints { make in
make.top.equalTo(self.navView.snp.bottom).offset(0) make.top.equalTo(self.navView.snp.bottom).offset(0)
make.left.right.equalToSuperview() make.left.right.equalToSuperview()
...@@ -59,15 +71,62 @@ class ContactsDupViewController : BaseViewController { ...@@ -59,15 +71,62 @@ class ContactsDupViewController : BaseViewController {
make.height.equalTo(statusBarHeight + 44) make.height.equalTo(statusBarHeight + 44)
} }
self.navView.addSubview(self.selectAllButton)
self.selectAllButton.snp.makeConstraints { make in
make.right.equalTo(-15 * RScreenW())
make.centerY.equalTo(self.navView.backButton.snp.centerY)
widthConstraint = make.width.equalTo(115).constraint
make.height.equalTo(32)
}
self.setDefaultPage() self.setDefaultPage()
self.selectAllButton.tapCallback = {[weak self] isSelected in
guard let self else {return}
DispatchQueue.main.async {
// 选择之后,更新宽度约束的常量值
if isSelected {
self.widthConstraint?.update(offset: 131)
if let data = self.dataSourceModel {
for(index,item) in data.enumerated(){
self.normalView.selectData[String(index)] = item
}
}
}else {
self.widthConstraint?.update(offset: 115)
self.normalView.selectData = [:]
}
// 强制重新布局
UIView.animate(withDuration: 0.3) {
self.selectAllButton.setNeedsLayout()
self.selectAllButton.layoutIfNeeded()
}
self.normalView.tableView.reloadData()
}
}
self.normalView.dataChangeCallBack = {[weak self]changed in
guard let self else {return}
DispatchQueue.main.async {
// 选择之后,更新宽度约束的常量值
if changed {
self.widthConstraint?.update(offset: 131)
self.selectAllButton.isSelected = true
}else {
self.widthConstraint?.update(offset: 115)
self.selectAllButton.isSelected = false
}
}
}
let dataUpdated = Notification.Name(ContactDupPreNormalView.CONTACT_MERGED)
NotificationCenter.default.addObserver(self, selector: #selector(handleDataUpdated(_:)), name: dataUpdated, object: nil)
} }
} }
extension ContactsDupViewController { extension ContactsDupViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
// 重新获取下重复项的数据
super.viewWillAppear(animated) super.viewWillAppear(animated)
if let data = self.dataSourceModel { if let data = self.dataSourceModel {
if data.count > 0 { if data.count > 0 {
...@@ -84,11 +143,7 @@ extension ContactsDupViewController { ...@@ -84,11 +143,7 @@ extension ContactsDupViewController {
}else { }else {
self.setDefaultPage() self.setDefaultPage()
} }
} }
func getCountFromDataSource(data:[[ContactModel]]) -> Int{ func getCountFromDataSource(data:[[ContactModel]]) -> Int{
var totalElementCount = 0 var totalElementCount = 0
for subArray in data { for subArray in data {
...@@ -96,5 +151,57 @@ extension ContactsDupViewController { ...@@ -96,5 +151,57 @@ extension ContactsDupViewController {
} }
return totalElementCount return totalElementCount
} }
func reGetCurrentPageData() {
// 重新获取下重复项的数据
let store = CNContactStore()
let keysToFetch = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
do {
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
// 创建数组
var duplicates : [[ContactModel]] = []
var contactsByName: [String: [ContactModel]] = [:]
try store.enumerateContacts(with: request) { contact, stop in
if let model = ContactModel.init(contact: contact) {
if !model.fullName.isEmpty {
if contactsByName[model.fullName] == nil {
contactsByName[model.fullName] = [model]
} else {
contactsByName[model.fullName]?.append(model)
}
}
duplicates = contactsByName.values.filter { $0.count > 1 }
}
}
DispatchQueue.main.async {
self.dataSourceModel = duplicates
self.normalView.dataSourceModel = duplicates
self.normalView.subTitleLabel.text = "\(self.getCountFromDataSource(data:duplicates)) Contacts"
self.normalView.selectData = [:]
if duplicates.count > 0 {
self.setNormalPage()
self.normalView.tableView.reloadData()
}else{
self.setDefaultPage()
}
}
} catch {
DispatchQueue.main.async {
print("获取联系人信息时发生错误: \(error)")
}
}
}
@objc func handleDataUpdated(_ notification: Notification) {
reGetCurrentPageData()
}
} }
//
// CustomContactViewController.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import ContactsUI
// 步骤1: 创建子类继承 CNContactViewController
class CustomContactViewController: CNContactViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.allowsEditing = false
setupCancelButton()
}
// 自定义取消按钮
private func setupCancelButton() {
navigationItem.leftBarButtonItem = UIBarButtonItem(
title: "Cancel",
style: .plain,
target: self,
action: #selector(dismissEditor))
}
}
extension CustomContactViewController {
@objc private func dismissEditor() {
// // 发起通知刷新重复联系人页面数据
// let dataUpdated = Notification.Name(ContactDupPreNormalView.CONTACT_MERGED)
// NotificationCenter.default.post(name: dataUpdated, object: nil, userInfo: nil)
//
// // 发起通知刷新重复联系人预览页面数据
// let dataUpdatePre = Notification.Name(ContactDupPreNormalView.CONTACT_MERGED_PRE)
// NotificationCenter.default.post(name: dataUpdatePre, object: nil, userInfo: nil)
dismiss(animated: true, completion: nil)
}
// 使用自定义视图控制器
func presentCustomContactEditor(with identifier: String , viewController: UIViewController) {
let contactStore = CNContactStore()
let keys = [CNContactViewController.descriptorForRequiredKeys()]
do {
let contact = try contactStore.unifiedContact(withIdentifier: identifier, keysToFetch: keys)
let mutableContact = contact.mutableCopy() as! CNMutableContact
// 步骤4: 使用自定义子类初始化
let editVC = CustomContactViewController(for: mutableContact)
editVC.contactStore = contactStore
let navController = UINavigationController(rootViewController: editVC)
viewController.present(navController, animated: true)
} catch {
print("获取联系人失败: \(error)")
}
}
}
...@@ -10,28 +10,41 @@ import Contacts ...@@ -10,28 +10,41 @@ import Contacts
struct ContactModel : Codable,Equatable { struct ContactModel : Codable,Equatable {
// 联系人名字 // 联系人名字
var name: String var fullName: String
var givenName : String
var familyName : String
// 联系人电话 // 联系人电话
var phoneNumber : [String]? var phoneNumber : [String]?
// 唯一id // 唯一id
var identifier : String var identifier : String
init(name: String, phoneNumber: [String]? = nil, identifier: String) { init(from decoder: any Decoder) throws {
self.name = name let container = try decoder.container(keyedBy: CodingKeys.self)
self.phoneNumber = phoneNumber self.fullName = try container.decode(String.self, forKey: .fullName)
self.identifier = identifier self.givenName = try container.decode(String.self, forKey: .givenName)
self.familyName = try container.decode(String.self, forKey: .familyName)
self.phoneNumber = try container.decodeIfPresent([String].self, forKey: .phoneNumber)
self.identifier = try container.decode(String.self, forKey: .identifier)
} }
} }
extension ContactModel { extension ContactModel {
init?(contact: CNContact) { init?(contact: CNContact) {
let fullName = "\(contact.givenName) \(contact.familyName)".trimmingCharacters(in: .whitespaces) let fullName = "\(contact.givenName) \(contact.familyName)".trimmingCharacters(in: .whitespaces)
let givenName = "\(contact.givenName)".trimmingCharacters(in: .whitespaces)
let familyName = "\(contact.familyName)".trimmingCharacters(in: .whitespaces)
let phoneNumbers = contact.phoneNumbers.map { $0.value.stringValue } let phoneNumbers = contact.phoneNumbers.map { $0.value.stringValue }
let uniqueID = contact.identifier let uniqueID = contact.identifier
if fullName.isEmpty || phoneNumbers.isEmpty || uniqueID.isEmpty { if uniqueID.isEmpty {
return nil return nil
} }
self.name = fullName self.fullName = fullName
self.givenName = givenName
self.familyName = familyName
self.phoneNumber = phoneNumbers self.phoneNumber = phoneNumbers
self.identifier = uniqueID self.identifier = uniqueID
} }
......
//
// ContactManager.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import Foundation
import Contacts
import ContactsUI
enum ContactError: Error {
case unauthorized
case contactNotFound(ContactModel)
case executionFailed(Error)
}
enum MergePolicy {
case union // 合并所有非空字段
case priority(ContactModel) // 指定优先级联系人
}
enum ContactMergeError: Error {
case unauthorized
case emptyGroup
case contactNotFound(identifier: String)
case mergeConflict(field: String)
case systemError(Error)
}
class ContactManager {
// MARK: 删除联系人
static func batchDeleteContacts( _ contacts: [ContactModel],completion: @escaping (Result<[ContactModel], ContactError>) -> Void) {
let store = CNContactStore()
let queue = DispatchQueue(label: "com.contacts.deleteQueue", qos: .userInitiated)
queue.async {
// 前置权限检查
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
DispatchQueue.main.async {
completion(.failure(.unauthorized))
}
return
}
do {
let result = try performAtomicDeletion(store: store, contacts: contacts)
DispatchQueue.main.async {
completion(.success(result))
}
} catch let error as ContactError {
DispatchQueue.main.async {
completion(.failure(error))
}
} catch {
DispatchQueue.main.async {
completion(.failure(.executionFailed(error)))
}
}
}
}
private static func performAtomicDeletion(store: CNContactStore, contacts: [ContactModel]) throws -> [ContactModel] {
let batchSize = 200
var deletedContacts = [ContactModel]()
for batch in contacts.chunked(into: batchSize) {
let identifiers = batch.map { $0.identifier }
// 批量获取联系人
let predicate = CNContact.predicateForContacts(withIdentifiers: identifiers)
let matchedContacts = try store.unifiedContacts(
matching: predicate,
keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor]
)
// 验证完整性
let matchedIDs = Set(matchedContacts.map { $0.identifier })
if let missing = batch.first(where: { !matchedIDs.contains($0.identifier) }) {
throw ContactError.contactNotFound(missing)
}
// 执行删除
let saveRequest = CNSaveRequest()
matchedContacts.forEach {
saveRequest.delete($0.mutableCopy() as! CNMutableContact)
}
try store.execute(saveRequest)
deletedContacts.append(contentsOf: batch)
}
return deletedContacts
}
// MARK: 合并联系人
static func mergeContacts(groups: [[ContactModel]],mergePolicy: MergePolicy = .union,completion: @escaping (Result<[String],ContactMergeError>) -> Void) {
let store = CNContactStore()
let queue = DispatchQueue(label: "com.contacts.mergeQueue", qos: .userInitiated)
queue.async {
// 权限检查
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
DispatchQueue.main.async { completion(.failure(.unauthorized)) }
return
}
do {
var mergedIDs = [String]()
let saveRequest = CNSaveRequest()
try groups.forEach { group in
guard !group.isEmpty else { throw ContactMergeError.emptyGroup }
// 获取原始联系人
let contacts = try fetchContacts(for: group, store: store)
// 创建合并后的联系人
let mergedContact = try mergeContacts(contacts, policy: mergePolicy)
// 删除出去第一个的联系人(因为第一个的identifier就是mergedContact的identifier,直接更新这个主联系人就可以了)
for(index,item) in contacts.enumerated(){
if(index != 0){
saveRequest.delete(item.mutableCopy() as! CNMutableContact)
}
}
// 更新主(传入的第一个)联系人
saveRequest.update(mergedContact)
mergedIDs.append(mergedContact.identifier)
}
try store.execute(saveRequest)
DispatchQueue.main.async { completion(.success(mergedIDs)) }
} catch let error as ContactMergeError {
DispatchQueue.main.async { completion(.failure(error)) }
} catch {
DispatchQueue.main.async { completion(.failure(.systemError(error))) }
}
}
}
private static func fetchContacts(for group: [ContactModel], store: CNContactStore) throws -> [CNContact] {
let identifiers = group.map { $0.identifier }
let predicate = CNContact.predicateForContacts(withIdentifiers: identifiers)
let keysToFetch: [CNKeyDescriptor] = [
CNContactGivenNameKey, CNContactFamilyNameKey,
CNContactPhoneNumbersKey, CNContactEmailAddressesKey,
CNContactPostalAddressesKey, CNContactIdentifierKey
].map { $0 as CNKeyDescriptor }
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
// 验证所有联系人存在
let foundIDs = Set(contacts.map { $0.identifier })
guard let missing = identifiers.first(where: { !foundIDs.contains($0) }) else {
return contacts
}
throw ContactMergeError.contactNotFound(identifier: missing)
}
private static func mergeContacts(_ contacts: [CNContact], policy: MergePolicy) throws -> CNMutableContact {
guard let baseContact = contacts.first else { throw ContactMergeError.emptyGroup }
let merged = baseContact.mutableCopy() as! CNMutableContact
switch policy {
case .union:
for contact in contacts{
mergePhoneNumbers(from: contact, to: merged)
mergeEmails(from: contact, to: merged)
mergeAddresses(from: contact, to: merged)
mergeNames(from: contact, to: merged)
}
case .priority(let model):
guard let priorityContact = contacts.first(where: { $0.identifier == model.identifier }) else {
throw ContactMergeError.contactNotFound(identifier: model.identifier)
}
copyContactProperties(from: priorityContact, to: merged)
}
return merged
}
private static func mergePhoneNumbers(from source: CNContact, to target: CNMutableContact) {
let existingNumbers = Set(target.phoneNumbers.map { $0.value.stringValue })
let newNumbers = source.phoneNumbers.filter {
!existingNumbers.contains($0.value.stringValue)
}
target.phoneNumbers.append(contentsOf: newNumbers)
}
private static func mergeEmails(from source: CNContact, to target: CNMutableContact) {
let existingEmails = Set(target.emailAddresses.map { $0.value as String })
let newEmails = source.emailAddresses.filter {
!existingEmails.contains($0.value as String)
}
target.emailAddresses.append(contentsOf: newEmails)
}
private static func mergeAddresses(from source: CNContact, to target: CNMutableContact) {
let existingAddresses = Set(target.postalAddresses.map { $0.value })
let newAddresses = source.postalAddresses.filter {
!existingAddresses.contains($0.value)
}
target.postalAddresses.append(contentsOf: newAddresses)
}
private static func mergeNames(from source: CNContact, to target: CNMutableContact) {
// 保留非空字段
if target.givenName.isEmpty && !source.givenName.isEmpty {
target.givenName = source.givenName
}
if target.familyName.isEmpty && !source.familyName.isEmpty {
target.familyName = source.familyName
}
}
private static func copyContactProperties(from source: CNContact, to target: CNMutableContact) {
// 基础信息
target.givenName = source.givenName
target.familyName = source.familyName
target.middleName = source.middleName
target.namePrefix = source.namePrefix
target.nameSuffix = source.nameSuffix
target.nickname = source.nickname
// 组织信息
target.organizationName = source.organizationName
target.departmentName = source.departmentName
target.jobTitle = source.jobTitle
// 联系方式
target.phoneNumbers = source.phoneNumbers.map {
CNLabeledValue(label: $0.label, value: $0.value)
}
target.emailAddresses = source.emailAddresses.map {
CNLabeledValue(label: $0.label, value: $0.value)
}
target.postalAddresses = source.postalAddresses.map {
CNLabeledValue(label: $0.label, value: $0.value)
}
// 其他信息
target.urlAddresses = source.urlAddresses.map {
CNLabeledValue(label: $0.label, value: $0.value)
}
target.contactRelations = source.contactRelations.map {
CNLabeledValue(label: $0.label, value: $0.value)
}
target.socialProfiles = source.socialProfiles.map {
CNLabeledValue(label: $0.label, value: $0.value)
}
// 日期信息
target.birthday = source.birthday
target.dates = source.dates.map {
CNLabeledValue(label: $0.label, value: $0.value)
}
// 备注
target.note = source.note
}
// MARK: 导入联系人
// 核心原子操作
static func performAtomicRestore(_ contacts: [ContactModel], completion: @escaping (Bool, Error?) -> Void) {
do {
let contactStore = CNContactStore()
// 1. 获取所有现有联系人ID
let existingContactIDs = try ContactManager.getAllContactIDs()
// 2. 创建统一事务请求
let saveRequest = CNSaveRequest()
// 3. 添加删除旧联系人的操作
existingContactIDs.forEach { id in
if let contact = try? contactStore.unifiedContact(withIdentifier: id, keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor]) {
let mutableContact = contact.mutableCopy() as! CNMutableContact
saveRequest.delete(mutableContact)
}
}
// 4. 添加新联系人
for model in contacts {
let newContact = ContactManager.createCNContact(from: model)
saveRequest.add(newContact, toContainerWithIdentifier: nil)
}
// 5. 执行事务(原子性)
try contactStore.execute(saveRequest)
completion(true, nil)
} catch {
// 事务失败,所有操作自动回滚
completion(false, error)
}
}
private static func getAllContactIDs() throws -> [String] {
let contactStore = CNContactStore()
let request = CNContactFetchRequest(keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor])
var contactIDs = [String]()
try contactStore.enumerateContacts(with: request) { contact, _ in
contactIDs.append(contact.identifier)
}
return contactIDs
}
private static func createCNContact(from model: ContactModel) -> CNMutableContact {
let contact = CNMutableContact()
contact.givenName = model.givenName
contact.familyName = model.familyName
if let numbers = model.phoneNumber {
contact.phoneNumbers = numbers.map { number in
CNLabeledValue(label: CNLabelPhoneNumberMain, value: CNPhoneNumber(stringValue: number))
}
}
return contact
}
// MARK: 根据id获取联系人
static func fetchContactFromStore(model: ContactModel)->CNContact? {
let contactStore = CNContactStore()
// 指定需要获取的联系人字段(示例包含姓名和电话)
let keysToFetch: [CNKeyDescriptor] = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
// 创建谓词(根据 identifier)
let predicate = CNContact.predicateForContacts(withIdentifiers: [model.identifier])
do {
let contacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
// 返回第一个匹配的联系人
return contacts.first
} catch {
print("获取联系人失败:\(error)")
return nil
}
}
// MARK: 其他方法
static func callContactWithIdentifier(_ model: ContactModel, viewController: UIViewController) {
let editViewController = CustomContactViewController()
editViewController.presentCustomContactEditor(with: model.identifier, viewController: viewController)
}
}
...@@ -6,10 +6,20 @@ ...@@ -6,10 +6,20 @@
// //
import Foundation import Foundation
import SnapKit
class ContactAllView : UIView { class ContactAllView : UIView {
static let CONTACT_ALL = "contact_all"
private var bottomConstraint: Constraint?
var dataSourceModel : [ContactModel] = [] var dataSourceModel : [ContactModel] = []
private var tabBottomConstraint: Constraint?
var dataClearCallBack : ()->Void = {}
/// 分组后的联系人 /// 分组后的联系人
private var sectionedContacts: [String: [ContactModel]] = [:] private var sectionedContacts: [String: [ContactModel]] = [:]
...@@ -59,10 +69,6 @@ class ContactAllView : UIView { ...@@ -59,10 +69,6 @@ class ContactAllView : UIView {
lazy var deleteButton : DeleteButtonView = { lazy var deleteButton : DeleteButtonView = {
let deleteButton = DeleteButtonView() let deleteButton = DeleteButtonView()
// 设置删除按钮
deleteButton.layer.cornerRadius = 23
deleteButton.clipsToBounds = true
deleteButton.isHidden = true
return deleteButton return deleteButton
}() }()
...@@ -91,13 +97,12 @@ class ContactAllView : UIView { ...@@ -91,13 +97,12 @@ class ContactAllView : UIView {
make.top.equalTo(self.subTitleLabel.snp.bottom).offset(16 * RScreenH()) make.top.equalTo(self.subTitleLabel.snp.bottom).offset(16 * RScreenH())
make.left.equalToSuperview().offset(15 * RScreenW()) make.left.equalToSuperview().offset(15 * RScreenW())
make.right.equalToSuperview().offset(-15 * RScreenW()) make.right.equalToSuperview().offset(-15 * RScreenW())
make.bottom.equalToSuperview().offset(-102 * RScreenH()) self.tabBottomConstraint = make.bottom.equalToSuperview().offset(-safeHeight).constraint
} }
self.deleteButton.snp.makeConstraints { make in self.deleteButton.snp.makeConstraints { make in
make.top.equalTo(self.tableView.snp.bottom).offset(16 * RScreenH()) make.left.right.equalToSuperview()
make.width.equalTo(345 * RScreenW()) self.bottomConstraint = make.bottom.equalToSuperview().offset(68).constraint
make.height.equalTo(46) make.height.equalTo(68)
make.centerX.equalToSuperview()
} }
// 排序 // 排序
...@@ -125,12 +130,17 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -125,12 +130,17 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
return sectionedContacts[sectionTitle]?.count ?? 0 return sectionedContacts[sectionTitle]?.count ?? 0
} }
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomContactAllViewTableViewCell
cell.selectContact(cell.selectButton)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomContactAllViewTableViewCell", for: indexPath) as! CustomContactAllViewTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "CustomContactAllViewTableViewCell", for: indexPath) as! CustomContactAllViewTableViewCell
let sectionTitle = sectionTitles[indexPath.section] let sectionTitle = sectionTitles[indexPath.section]
let contact = sectionedContacts[sectionTitle]?[indexPath.row] let contact = sectionedContacts[sectionTitle]?[indexPath.row]
cell.model = contact cell.model = contact
cell.nameLabel.text = contact?.name cell.nameLabel.text = contact?.fullName
if self.selectedContacts.contains(where: { $0.identifier == contact!.identifier }) { if self.selectedContacts.contains(where: { $0.identifier == contact!.identifier }) {
cell.selectButton.isSelected = true cell.selectButton.isSelected = true
}else { }else {
...@@ -143,16 +153,25 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -143,16 +153,25 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
}else{ }else{
self.selectedContacts.removeAll(where: { $0.identifier == model.identifier }) self.selectedContacts.removeAll(where: { $0.identifier == model.identifier })
} }
DispatchQueue.main.async { DispatchQueue.main.async {
// 判断button是否显示 // 判断button是否显示
if self.selectedContacts.count > 0 { if self.selectedContacts.count > 0 {
// 设置button的title // 设置button的title
self.deleteButton.titleLabel.text = "Delete \(self.selectedContacts.count) Contact" self.deleteButton.deleteButton.setTitle("Delete \(self.selectedContacts.count) Contact", for: .normal)
self.deleteButton.isHidden = false UIView.animate(withDuration: 0.1) {
// 更新约束
self.bottomConstraint?.update(offset: -safeHeight)
self.tabBottomConstraint?.update(offset: -safeHeight - 68)
self.layoutIfNeeded()
}
}else{ }else{
UIView.animate(withDuration: 0.1) {
self.deleteButton.isHidden = true // 更新约束
self.bottomConstraint?.update(offset: 68)
self.tabBottomConstraint?.update(offset: -safeHeight)
self.layoutIfNeeded()
}
} }
} }
...@@ -176,6 +195,7 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -176,6 +195,7 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
return 20 return 20
} }
func setupCustomIndexView() { func setupCustomIndexView() {
customIndexView = nil
customIndexView = UIStackView() customIndexView = UIStackView()
customIndexView.axis = .vertical customIndexView.axis = .vertical
customIndexView.alignment = .center customIndexView.alignment = .center
...@@ -185,9 +205,9 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -185,9 +205,9 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
self.customIndexView.snp.makeConstraints { make in self.customIndexView.snp.makeConstraints { make in
make.left.equalTo(self.tableView.snp.right).offset(0) make.left.equalTo(self.tableView.snp.right).offset(0)
make.top.equalTo(self.subTitleLabel.snp.bottom).offset(77) make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(15 * RScreenW()) make.width.equalTo(15 * RScreenW())
make.height.equalTo(354) make.height.equalTo(self.sectionTitles.count * (14 + 2))
} }
for (index, section) in sectionTitles.enumerated() { for (index, section) in sectionTitles.enumerated() {
...@@ -212,8 +232,17 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -212,8 +232,17 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
} }
} }
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleRect = CGRect(origin: tableView.contentOffset, size: tableView.bounds.size) // 获取可见区域的中心点,稍微向下偏移以更准确地检测当前可见的 section
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.minY) let visibleRect = CGRect(
origin: tableView.contentOffset,
size: tableView.bounds.size
)
// 将检测点从顶部边缘下移一些,例如下移10%的可见区域高度
let adjustedY = visibleRect.minY + visibleRect.height * 0.1
let visiblePoint = CGPoint(x: visibleRect.midX, y: adjustedY)
// 获取可见区域最顶部的单元格的 indexPath
if let visibleIndexPath = tableView.indexPathForRow(at: visiblePoint) { if let visibleIndexPath = tableView.indexPathForRow(at: visiblePoint) {
let newIndex = visibleIndexPath.section let newIndex = visibleIndexPath.section
if newIndex != selectedIndex { if newIndex != selectedIndex {
...@@ -237,7 +266,6 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -237,7 +266,6 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
let indexPath = IndexPath(row: 0, section: index) let indexPath = IndexPath(row: 0, section: index)
tableView.scrollToRow(at: indexPath, at: .top, animated: true) tableView.scrollToRow(at: indexPath, at: .top, animated: true)
selectedIndex = index selectedIndex = index
updateIndexStyles()
} }
} }
...@@ -251,12 +279,32 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -251,12 +279,32 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
guard let self else {return} guard let self else {return}
if isSure { if isSure {
backupContactsByselect { backupContactsByselect {
// 备份完成后删除 // 删除之前弹出是否真的需要删除
self.deleteContacts() let alertVc = ContactDeleteAlertView()
alertVc.frame = (self.responderViewController()?.view.bounds)!
cWindow?.addSubview(alertVc)
alertVc.sureCallBack = {[weak self] isSure in
guard let self else {return}
if isSure {
// 提示是否删除
self.deleteContacts()
}
}
} }
}else{ }else{
// 如果不备份,直接删除 // 删除之前弹出是否真的需要删除
self.deleteContacts() let alertVc = ContactDeleteAlertView()
alertVc.frame = (self.responderViewController()?.view.bounds)!
cWindow?.addSubview(alertVc)
alertVc.sureCallBack = {[weak self] isSure in
guard let self else {return}
if isSure {
// 提示是否删除
self.deleteContacts()
}
}
} }
} }
} }
...@@ -264,15 +312,15 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -264,15 +312,15 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
func backupContactsByselect(success:@escaping()->Void){ func backupContactsByselect(success:@escaping()->Void){
// 开始备份联系人,备份完成提示 // 开始备份联系人,备份完成提示
let vm = BackupViewModel() let vm = BackupViewModel()
vm.backupPartialContacts(self.selectedContacts) { finised, error in vm.backupPartialContacts(self.dataSourceModel) { finised, error in
if finised { if finised {
// 备份成功 // 备份成功
success()
DispatchQueue.main.async { DispatchQueue.main.async {
let buAlertVc = ContactBackUpCompletedAlertView(frame: (self.responderViewController()?.view.bounds)!) let buAlertVc = ContactBackUpCompletedAlertView(frame: (cWindow?.bounds)!)
self.responderViewController()?.view.addSubview(buAlertVc) cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
buAlertVc.removeFromSuperview() buAlertVc.removeFromSuperview()
success()
} }
} }
}else{ }else{
...@@ -297,11 +345,61 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -297,11 +345,61 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
} }
} }
} }
selectedContacts.removeAll()
sortContacts()
self.tableView.reloadData()
// fixme: 需要从联系人列表中删除 // fixme: 需要从联系人列表中删除
ContactManager.batchDeleteContacts(self.selectedContacts) { result in
switch result {
case .success(let deletedContacts):
print("成功删除 \(deletedContacts.count) 个联系人")
self.selectedContacts.removeAll()
self.sortContacts()
self.subTitleLabel.text = "\(self.dataSourceModel.count) Contacts"
self.updateDeleteButtonStatus()
self.tableView.reloadData()
if self.dataSourceModel.count <= 0 {
self.dataClearCallBack()
}
// 删除完成 弹窗
let buAlertVc = ContactBackUpDeleteCompletedAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
buAlertVc.removeFromSuperview()
}
break
case .failure(.contactNotFound(let missing)):
print("操作终止:未找到联系人 \(missing.identifier)")
case .failure(.unauthorized):
print("无通讯录访问权限")
case .failure(.executionFailed(let error)):
print("操作失败:\(error.localizedDescription)")
}
}
}
func updateDeleteButtonStatus() {
DispatchQueue.main.async {
// 判断button是否显示
if self.selectedContacts.count > 0 {
// 设置button的title
self.deleteButton.deleteButton.setTitle("Delete \(self.selectedContacts.count) Contact", for: .normal)
UIView.animate(withDuration: 0.1) {
// 更新约束
self.bottomConstraint?.update(offset: -safeHeight)
self.tabBottomConstraint?.update(offset: -safeHeight - 68)
self.layoutIfNeeded()
}
}else{
UIView.animate(withDuration: 0.1) {
// 更新约束
self.bottomConstraint?.update(offset: 68)
self.tabBottomConstraint?.update(offset: -safeHeight)
self.layoutIfNeeded()
}
}
}
} }
...@@ -309,7 +407,7 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -309,7 +407,7 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
func sortContacts() { func sortContacts() {
sectionedContacts.removeAll() sectionedContacts.removeAll()
for contact in self.dataSourceModel { for contact in self.dataSourceModel {
let firstLetter = pinyinFirstLetter(contact.name).uppercased() let firstLetter = pinyinFirstLetter(contact.fullName).uppercased()
if sectionedContacts[firstLetter] == nil { if sectionedContacts[firstLetter] == nil {
sectionedContacts[firstLetter] = [] sectionedContacts[firstLetter] = []
} }
...@@ -319,7 +417,7 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate { ...@@ -319,7 +417,7 @@ extension ContactAllView : UITableViewDataSource,UITableViewDelegate {
sectionTitles = sectionedContacts.keys.sorted() sectionTitles = sectionedContacts.keys.sorted()
for key in sectionTitles { for key in sectionTitles {
sectionedContacts[key] = sectionedContacts[key]?.sorted { sectionedContacts[key] = sectionedContacts[key]?.sorted {
return pinyinFirstLetter($0.name).uppercased() < pinyinFirstLetter($1.name).uppercased() return pinyinFirstLetter($0.fullName).uppercased() < pinyinFirstLetter($1.fullName).uppercased()
} }
} }
} }
......
...@@ -9,6 +9,8 @@ import Foundation ...@@ -9,6 +9,8 @@ import Foundation
class ContactBackUpNormalView : UIView { class ContactBackUpNormalView : UIView {
var isClearAllCallBack : ()->Void = {}
var dataSourceModel : [BackupInfoModel] = [] var dataSourceModel : [BackupInfoModel] = []
var dataSourceAllModel : [ContactModel]? var dataSourceAllModel : [ContactModel]?
...@@ -76,7 +78,7 @@ class ContactBackUpNormalView : UIView { ...@@ -76,7 +78,7 @@ class ContactBackUpNormalView : UIView {
make.top.equalTo(self.addButon.snp.bottom).offset(16) make.top.equalTo(self.addButon.snp.bottom).offset(16)
make.left.equalToSuperview().offset(15 * RScreenW()) make.left.equalToSuperview().offset(15 * RScreenW())
make.right.equalToSuperview().offset(-15 * RScreenW()) make.right.equalToSuperview().offset(-15 * RScreenW())
make.bottom.equalToSuperview() make.bottom.equalToSuperview().offset(-safeHeight)
} }
} }
} }
...@@ -125,11 +127,16 @@ extension ContactBackUpNormalView : UITableViewDelegate,UITableViewDataSource{ ...@@ -125,11 +127,16 @@ extension ContactBackUpNormalView : UITableViewDelegate,UITableViewDataSource{
} }
DispatchQueue.main.async { DispatchQueue.main.async {
// 弹框提示成功 // 弹框提示成功
let buAlertVc = ContactBackUpDeleteCompletedAlertView(frame: (self.responderViewController()?.view.bounds)!) let buAlertVc = ContactBackUpDeleteCompletedAlertView(frame: (cWindow?.bounds)!)
self.responderViewController()?.view.addSubview(buAlertVc) cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
buAlertVc.removeFromSuperview() buAlertVc.removeFromSuperview()
} }
// 成功之后给上游发消息
if self.dataSourceModel.count <= 0 {
self.isClearAllCallBack()
}
} }
} }
} }
...@@ -166,15 +173,33 @@ extension ContactBackUpNormalView : UITableViewDelegate,UITableViewDataSource{ ...@@ -166,15 +173,33 @@ extension ContactBackUpNormalView : UITableViewDelegate,UITableViewDataSource{
@objc func addBackupAction() { @objc func addBackupAction() {
let vm = BackupViewModel() let vm = BackupViewModel()
vm.backupAllContacts(self.dataSourceAllModel ?? []) { finished, error in
DispatchQueue.main.async { // 判断当前是否有可用的备份数据
let buAlertVc = ContactBackUpCompletedAlertView(frame: (self.responderViewController()?.view.bounds)!) if let data = self.dataSourceAllModel {
self.responderViewController()?.view.addSubview(buAlertVc) if data.count > 0 {
vm.backupAllContacts(self.dataSourceAllModel ?? []) { finished, error in
DispatchQueue.main.async {
let buAlertVc = ContactBackUpCompletedAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
buAlertVc.removeFromSuperview()
}
// 再次请求数据 重新刷新页面
self.updateData()
}
}
}else {
let buAlertVc = ContactBackUpNoDataAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
buAlertVc.removeFromSuperview() buAlertVc.removeFromSuperview()
} }
// 再次请求数据 重新刷新页面 }
self.updateData() }else {
let buAlertVc = ContactBackUpNoDataAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
buAlertVc.removeFromSuperview()
} }
} }
} }
......
...@@ -99,7 +99,9 @@ class BackupViewModel { ...@@ -99,7 +99,9 @@ class BackupViewModel {
decoder.dateDecodingStrategy = .iso8601 decoder.dateDecodingStrategy = .iso8601
// 尝试解码为 BackupInfoModel 数组 // 尝试解码为 BackupInfoModel 数组
let backupInfos = try decoder.decode([BackupInfoModel].self, from: jsonData) let backupInfos = try decoder.decode([BackupInfoModel].self, from: jsonData)
completion(backupInfos, nil) // 根据时间倒序下
let sortedModels = backupInfos.sorted { $0.backupTime > $1.backupTime }
completion(sortedModels, nil)
} catch { } catch {
Print(error.localizedDescription) Print(error.localizedDescription)
completion(nil, error) completion(nil, error)
......
...@@ -72,7 +72,7 @@ class CustomContactAllViewTableViewCell : UITableViewCell { ...@@ -72,7 +72,7 @@ class CustomContactAllViewTableViewCell : UITableViewCell {
} }
extension CustomContactAllViewTableViewCell { extension CustomContactAllViewTableViewCell {
@objc private func selectContact(_ sender: UIButton) { @objc func selectContact(_ sender: UIButton) {
sender.isSelected = !sender.isSelected sender.isSelected = !sender.isSelected
buttonSelectCallBack(model!,sender.isSelected) buttonSelectCallBack(model!,sender.isSelected)
} }
......
//
// CustomContactDupTableViewCell.swift
// PhoneManager
//
// Created by edy on 2025/5/6.
//
import Foundation
class CustomContactDupPreTableViewCell : UITableViewCell {
var indexPath: IndexPath?
var cellSelectCallCack : (IndexPath,Bool)->Void = {index,choose in}
var model : [ContactModel] = [] {
didSet{
DispatchQueue.main.async {
if self.model.count > 0 {
self.nameLabel.text = self.model.first?.fullName
self.numberLabel.text = self.getPhoneNumberString(contacts: self.model)
}else {
self.nameLabel.text = ""
self.numberLabel.text = ""
}
}
}
}
lazy var backView : UIView = {
let view = UIView()
view.backgroundColor = UIColor(red: 0.95, green: 0.96, blue: 0.99, alpha: 1)
view.clipsToBounds = true
view.layer.cornerRadius = 12
return view
}()
lazy var nameLabel : UILabel = {
let label = UILabel()
label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
label.font = UIFont.systemFont(ofSize: 16, weight: .bold)
return label
}()
lazy var numberLabel : UILabel = {
let label = UILabel()
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1)
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
return label
}()
lazy var moreButton : UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "icon_left_setting_grey"), for: .normal)
return button
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
self.contentView.addSubview(self.backView)
self.backView.addSubview(self.nameLabel)
self.backView.addSubview(self.numberLabel)
self.backView.addSubview(self.moreButton)
self.backView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(71)
}
self.nameLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16 * RScreenH())
make.top.equalToSuperview().offset(16)
make.width.equalTo(210)
make.height.equalTo(22)
}
self.numberLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16 * RScreenH())
make.top.equalTo(self.nameLabel.snp.bottom).offset(0)
make.right.equalTo(self.moreButton.snp.left).offset(0)
make.height.equalTo(20)
}
self.moreButton.snp.makeConstraints { make in
make.width.height.equalTo(24)
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-16 * RScreenW())
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension CustomContactDupPreTableViewCell {
func getPhoneNumberString(contacts : [ContactModel]) -> String{
var photoNumbers : [String] = []
for item in contacts {
if let tempNumbers = item.phoneNumber {
photoNumbers = photoNumbers + tempNumbers
}
}
let nonEmptyStrings = photoNumbers.filter {!$0.isEmpty}
if nonEmptyStrings.count <= 0 {
return ""
}
return photoNumbers.joined(separator: " / ")
}
}
...@@ -10,10 +10,13 @@ import Foundation ...@@ -10,10 +10,13 @@ import Foundation
class CustomContactDupTableViewCell : UITableViewCell { class CustomContactDupTableViewCell : UITableViewCell {
var indexPath: IndexPath?
var cellSelectCallCack : (IndexPath,Bool)->Void = {index,choose in}
var model : ContactModel?{ var model : ContactModel?{
didSet{ didSet{
self.nameLabel.text = model?.name self.nameLabel.text = model?.fullName
if let numbers = model?.phoneNumber { if let numbers = model?.phoneNumber {
self.numberLabel.text = getPhoneNumberString(photoNumbers:numbers) self.numberLabel.text = getPhoneNumberString(photoNumbers:numbers)
}else { }else {
...@@ -78,7 +81,7 @@ class CustomContactDupTableViewCell : UITableViewCell { ...@@ -78,7 +81,7 @@ class CustomContactDupTableViewCell : UITableViewCell {
self.numberLabel.snp.makeConstraints { make in self.numberLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16 * RScreenH()) make.left.equalToSuperview().offset(16 * RScreenH())
make.top.equalTo(self.nameLabel.snp.bottom).offset(0) make.top.equalTo(self.nameLabel.snp.bottom).offset(0)
make.width.equalTo(210) make.right.equalTo(self.selectButton.snp.left).offset(0)
make.height.equalTo(20) make.height.equalTo(20)
} }
...@@ -97,9 +100,11 @@ class CustomContactDupTableViewCell : UITableViewCell { ...@@ -97,9 +100,11 @@ class CustomContactDupTableViewCell : UITableViewCell {
} }
extension CustomContactDupTableViewCell { extension CustomContactDupTableViewCell {
@objc private func selectContact(_ sender: UIButton) { @objc func selectContact(_ sender: UIButton) {
sender.isSelected = !sender.isSelected sender.isSelected = !sender.isSelected
// buttonSelectCallBack(model!,sender.isSelected) if let indexPath = self.indexPath {
self.cellSelectCallCack(indexPath,sender.isSelected)
}
} }
......
//
// CustomDupHeaderView.swift
// PhoneManager
//
// Created by edy on 2025/5/7.
//
import Foundation
class CustomDupHeaderView : UITableViewHeaderFooterView {
var model : [ContactModel] = [] {
didSet{
DispatchQueue.main.async {
self.tiplabel.text = "\(self.model.count) Duplicate Contacts"
}
}
}
var headerCallback : ([ContactModel])->Void = {array in}
var selectStatus : Bool = false
lazy var tiplabel : UILabel = {
let label = UILabel()
label.text = "\(self.model.count) Duplicate Contacts"
label.textAlignment = .left
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1)
return label
}()
lazy var subLabel : UILabel = {
let selectLabel = UILabel()
selectLabel.textAlignment = .right
selectLabel.textColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
if self.selectStatus {
selectLabel.text = "Select All"
}else{
selectLabel.text = "Deselect All"
}
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(cellSelectTap))
selectLabel.isUserInteractionEnabled = true
selectLabel.addGestureRecognizer(tap)
return selectLabel
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
self.addSubview(self.tiplabel)
self.tiplabel.snp.makeConstraints { make in
make.left.top.equalToSuperview()
make.width.equalTo(200)
make.height.equalTo(22)
}
self.addSubview(self.subLabel)
self.subLabel.snp.makeConstraints { make in
make.width.equalTo(100)
make.height.equalTo(22)
make.right.equalToSuperview()
make.centerY.equalTo(self.tiplabel.snp.centerY)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension CustomDupHeaderView {
@objc func cellSelectTap(){
if self.selectStatus {
self.subLabel.text = "Deselect All"
}else {
self.subLabel.text = "Select All"
}
self.headerCallback(self.model)
}
}
//
// CustomDupHeaderView.swift
// PhoneManager
//
// Created by edy on 2025/5/7.
//
import Foundation
class CustomDupPreHeaderView : UITableViewHeaderFooterView {
var model : [ContactModel] = [] {
didSet{
DispatchQueue.main.async {
self.tiplabel.text = "\(self.model.count) Contacts Merged"
}
}
}
// 当前的section
var currentSection : Int = 0
var headerCallback : ([ContactModel],Int)->Void = {array,currentSection in}
var selectStatus : Bool = false
lazy var tiplabel : UILabel = {
let label = UILabel()
label.text = "\(self.model.count) Contacts Merged"
label.textAlignment = .left
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1)
return label
}()
lazy var deSelectButton : UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "ic_close_similar"), for: .normal)
button.addTarget(self, action: #selector(removeCurrentData), for: .touchUpInside)
return button
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
self.addSubview(self.tiplabel)
self.tiplabel.snp.makeConstraints { make in
make.left.top.equalToSuperview()
make.width.equalTo(200)
make.height.equalTo(22)
}
self.addSubview(self.deSelectButton)
self.deSelectButton.snp.makeConstraints { make in
make.width.equalTo(20)
make.height.equalTo(20)
make.right.equalToSuperview()
make.centerY.equalTo(self.tiplabel.snp.centerY)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension CustomDupPreHeaderView {
@objc func removeCurrentData(){
self.headerCallback(self.model,currentSection)
}
}
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
import Foundation import Foundation
class ContactBackUpCompletedAlertView : UIView { class ContactBackUpCompletedAlertView : UIView {
var sureCallBack: ()->Void = {}
// 懒加载背景视图 // 懒加载背景视图
private lazy var backgroundView: UIView = { private lazy var backgroundView: UIView = {
......
//
// ContactBackUpNoDataAlertView.swift
// PhoneManager
//
// Created by edy on 2025/5/9.
//
import Foundation
class ContactBackUpNoDataAlertView : UIView {
// 懒加载背景视图
private lazy var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
return view
}()
// 懒加载卡片视图
private lazy var cardView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.clipsToBounds = true
return view
}()
private lazy var imageView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.image = UIImage(named: "ic_no_duolicates")
return view
}()
// 懒加载标题标签
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "No backup data is available"
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5000)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
self.addSubview(backgroundView)
backgroundView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
self.addSubview(cardView)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalTo(175)
make.height.equalTo(115)
}
cardView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.width.height.equalTo(35)
make.centerX.equalToSuperview()
}
cardView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalTo(self.imageView.snp.bottom).offset(0)
make.left.right.equalToSuperview().inset(20)
make.height.equalTo(40)
}
}
}
//
// ContactMergeAlertView.swift
// PhoneManager
//
// Created by edy on 2025/5/9.
//
import Foundation
class ContactMergeAlertView : UIView {
var sureCallBack: ()->Void = {}
// 懒加载背景视图
private lazy var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
return view
}()
// 懒加载卡片视图
private lazy var cardView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.clipsToBounds = true
return view
}()
// 懒加载标题标签
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Are you sure you want to merge duplicate contacts?"
label.font = UIFont.systemFont(ofSize: 17, weight: .bold)
label.textColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1)
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
// 懒加载副标题标签
private lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.text = "This process cannot be reversed"
label.font = UIFont.systemFont(ofSize: 13, weight: .regular)
label.textColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1)
label.textAlignment = .center
return label
}()
// 懒加载取消按钮
private lazy var cancelButton: UIButton = {
let button = UIButton()
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .clear
button.setTitleColor(UIColor(red: 0, green: 0.48, blue: 1, alpha: 1), for: .normal)
button.layer.cornerRadius = 5
button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .regular)
button.addTarget(self, action: #selector(dismissAlert), for: .touchUpInside)
return button
}()
// 懒加载确认按钮
private lazy var yesButton: UIButton = {
let button = UIButton()
button.setTitle("Yes", for: .normal)
button.backgroundColor = .clear
button.setTitleColor(UIColor(red: 0, green: 0.48, blue: 1, alpha: 1), for: .normal)
button.layer.cornerRadius = 5
button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .regular)
button.addTarget(self, action: #selector(sureAlert), for: .touchUpInside)
return button
}()
// 懒加载顶部边线
private lazy var topBorder: UIView = {
let view = UIView()
view.backgroundColor = UIColor(red: 0.24, green: 0.24, blue: 0.26, alpha: 0.3600)
return view
}()
// 懒加载中间分割线
private lazy var middleBorder: UIView = {
let view = UIView()
view.backgroundColor = UIColor(red: 0.24, green: 0.24, blue: 0.26, alpha: 0.3600)
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5000)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
self.addSubview(backgroundView)
backgroundView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
self.addSubview(cardView)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalTo(270)
make.height.equalTo(154)
}
cardView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(16)
make.left.right.equalToSuperview().inset(16)
make.height.equalTo(44)
}
cardView.addSubview(subtitleLabel)
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(2)
make.left.right.equalToSuperview().inset(16)
make.height.equalTo(18)
}
cardView.addSubview(cancelButton)
cancelButton.snp.makeConstraints { make in
make.left.equalToSuperview()
make.bottom.equalToSuperview()
make.width.equalTo(cardView.snp.width).multipliedBy(0.5)
make.height.equalTo(44)
}
cardView.addSubview(yesButton)
yesButton.snp.makeConstraints { make in
make.right.equalToSuperview()
make.bottom.equalToSuperview()
make.width.equalTo(cardView.snp.width).multipliedBy(0.5)
make.height.equalTo(44)
}
cardView.addSubview(topBorder)
topBorder.snp.makeConstraints { make in
make.top.equalTo(cancelButton.snp.top)
make.left.right.equalTo(cardView)
make.height.equalTo(0.5)
}
cardView.addSubview(middleBorder)
middleBorder.snp.makeConstraints { make in
make.centerX.equalTo(cardView)
make.top.equalTo(cancelButton.snp.top)
make.bottom.equalTo(cancelButton.snp.bottom)
make.width.equalTo(0.5)
}
}
@objc private func dismissAlert() {
self.removeFromSuperview()
}
@objc private func sureAlert() {
self.removeFromSuperview()
sureCallBack()
}
}
//
// ContactRestoreSuccessView.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import Foundation
class ContactRestoreSuccessView : UIView {
// 懒加载背景视图
private lazy var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
return view
}()
// 懒加载卡片视图
private lazy var cardView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.clipsToBounds = true
return view
}()
private lazy var imageView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.image = UIImage(named: "ic_ok_duolicates")
return view
}()
// 懒加载标题标签
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Backup restored"
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1)
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5000)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
self.addSubview(backgroundView)
backgroundView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
self.addSubview(cardView)
cardView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalTo(175)
make.height.equalTo(95)
}
cardView.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.width.height.equalTo(35)
make.centerX.equalToSuperview()
}
cardView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalTo(self.imageView.snp.bottom).offset(0)
make.left.right.equalToSuperview().inset(20)
make.height.equalTo(20)
}
}
}
...@@ -10,43 +10,40 @@ class DeleteButtonView : UIView { ...@@ -10,43 +10,40 @@ class DeleteButtonView : UIView {
var submitCallBack : (()->Void) = {} var submitCallBack : (()->Void) = {}
lazy var imageView : UIImageView = { lazy var deleteButton : UIButton = {
let imageView = UIImageView() let view = UIButton(type: .custom)
imageView.image = UIImage(named: "ic_delete_duplicates") view.backgroundColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
return imageView view.setTitleColor(.white, for: .normal)
}() view.clipsToBounds = true
view.layer.cornerRadius = 23
lazy var titleLabel : UILabel = { view.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)
let label = UILabel() view.setTitle("See Merge Preview", for: .normal)
label.textAlignment = .left view.setImage(UIImage(named: "ic_hebing"), for: .normal)
label.textColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1)
return label // 设置间距为 8
let spacing: CGFloat = 8
// 获取图片和文字的大小
let imageSize = view.imageView?.image?.size ?? .zero
let titleSize = view.titleLabel?.intrinsicContentSize ?? .zero
// 计算 imageEdgeInsets 和 titleEdgeInsets
view.imageEdgeInsets = UIEdgeInsets(top: 0, left: -spacing / 2, bottom: 0, right: spacing / 2)
view.titleEdgeInsets = UIEdgeInsets(top: 0, left: spacing / 2, bottom: 0, right: -spacing / 2)
view.addTarget(self, action: #selector(deleteButtonAction), for: .touchUpInside)
return view
}() }()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame)
self.isUserInteractionEnabled = true
self.backgroundColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(submitAction))
self.addGestureRecognizer(tap)
self.addSubview(self.imageView)
self.addSubview(self.titleLabel)
super.init(frame: frame)
self.backgroundColor = .white
self.addSubview(self.deleteButton)
self.imageView.snp.makeConstraints { make in self.deleteButton.snp.makeConstraints { make in
make.width.height.equalTo(20) make.left.equalToSuperview().offset(15)
make.centerY.equalToSuperview() make.right.equalToSuperview().offset(-15)
make.left.equalToSuperview().offset(94 * RScreenW()) make.height.equalTo(46)
} make.top.equalToSuperview().offset(16)
self.titleLabel.snp.makeConstraints { make in
make.left.equalTo(self.imageView.snp.right).offset(8 * RScreenW())
make.centerY.equalToSuperview()
make.height.equalTo(22)
} }
} }
...@@ -60,7 +57,7 @@ class DeleteButtonView : UIView { ...@@ -60,7 +57,7 @@ class DeleteButtonView : UIView {
extension DeleteButtonView { extension DeleteButtonView {
@objc func submitAction(){ @objc func deleteButtonAction(){
submitCallBack() submitCallBack()
} }
} }
...@@ -6,19 +6,31 @@ ...@@ -6,19 +6,31 @@
// //
import Foundation import Foundation
import SnapKit
class ContactDupNormalView : UIView { class ContactDupNormalView : UIView {
var dataSourceModel : [[ContactModel]] = [] var dataSourceModel : [[ContactModel]] = []
var selectData: [Int : [ContactModel]] = [:] var preButtonShowStatus : Bool = false
/// 选择的联系人 var selectData: [String : [ContactModel]] = [:] {
private var selectedContacts: [ContactModel] = [] didSet {
// 是否更新底部合并预览按钮
showPreMergeButton()
self.dataChangeCallBack(isAllData())
}
}
var dataChangeCallBack:(Bool)->Void = {changed in}
private var bottomConstraint: Constraint?
private var tabBottomConstraint: Constraint?
lazy var titleLabel: UILabel = { lazy var titleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.text = "All contacts" label.text = "Duplicates"
label.font = UIFont.systemFont(ofSize: 20, weight: .bold) label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
label.textAlignment = .left label.textAlignment = .left
...@@ -48,11 +60,18 @@ class ContactDupNormalView : UIView { ...@@ -48,11 +60,18 @@ class ContactDupNormalView : UIView {
return tableView return tableView
}() }()
lazy var preButtonView : MergePreButtonView = {
let view = MergePreButtonView()
return view
}()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.titleLabel) self.addSubview(self.titleLabel)
self.addSubview(self.subTitleLabel) self.addSubview(self.subTitleLabel)
self.addSubview(self.tableView) self.addSubview(self.tableView)
self.addSubview(self.preButtonView)
self.titleLabel.snp.makeConstraints { make in self.titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15 * RScreenW()) make.left.equalToSuperview().offset(15 * RScreenW())
...@@ -72,14 +91,27 @@ class ContactDupNormalView : UIView { ...@@ -72,14 +91,27 @@ class ContactDupNormalView : UIView {
make.top.equalTo(self.subTitleLabel.snp.bottom).offset(16 * RScreenH()) make.top.equalTo(self.subTitleLabel.snp.bottom).offset(16 * RScreenH())
make.left.equalToSuperview().offset(15 * RScreenW()) make.left.equalToSuperview().offset(15 * RScreenW())
make.right.equalToSuperview().offset(-15 * RScreenW()) make.right.equalToSuperview().offset(-15 * RScreenW())
make.bottom.equalToSuperview().offset(-44 * RScreenH()) self.tabBottomConstraint = make.bottom.equalToSuperview().offset(-safeHeight).constraint
}
self.preButtonView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
self.bottomConstraint = make.bottom.equalToSuperview().offset(68).constraint
make.height.equalTo(68)
}
self.preButtonView.mergePreCallBack = {
let vc = ContactsDupPreViewController()
// 对data 做下处理 当里面只有一个元素的时候实际是不能合并的
vc.dataSourceModel = self.removeSingalDataInDataSource()
self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
} }
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
} }
extension ContactDupNormalView : UITableViewDelegate,UITableViewDataSource{ extension ContactDupNormalView : UITableViewDelegate,UITableViewDataSource{
...@@ -94,8 +126,27 @@ extension ContactDupNormalView : UITableViewDelegate,UITableViewDataSource{ ...@@ -94,8 +126,27 @@ extension ContactDupNormalView : UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomContactDupTableViewCell", for: indexPath) as! CustomContactDupTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "CustomContactDupTableViewCell", for: indexPath) as! CustomContactDupTableViewCell
cell.indexPath = indexPath
cell.model = self.dataSourceModel[indexPath.section][indexPath.row] cell.model = self.dataSourceModel[indexPath.section][indexPath.row]
cell.cellSelectCallCack = {[weak self]index,choose in
guard let self else {return}
if choose {
self.saveSelectModel(index: index)
}else {
self.removeSelectModel(index: index)
}
}
// 如果有选中的更新
if let tempArray = self.selectData[String(indexPath.section)] {
if tempArray.contains(where: {$0.identifier == self.dataSourceModel[indexPath.section][indexPath.row].identifier}) {
cell.selectButton.isSelected = true
}else{
cell.selectButton.isSelected = false
}
}else {
cell.selectButton.isSelected = false
}
return cell return cell
} }
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
...@@ -103,49 +154,154 @@ extension ContactDupNormalView : UITableViewDelegate,UITableViewDataSource{ ...@@ -103,49 +154,154 @@ extension ContactDupNormalView : UITableViewDelegate,UITableViewDataSource{
} }
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = UIView(frame: CGRect(x: 0 , y: 0, width: self.tableView.width, height: 22)) let view = CustomDupHeaderView(frame: CGRect(x: 0 , y: 0, width: self.tableView.width, height: 22))
view.backgroundColor = .clear view.model = self.dataSourceModel[section]
let label = UILabel(frame: CGRect(x: 0 , y: 0, width: 200, height: 22))
label.text = "\(self.dataSourceModel[section].count) Duplicate Contacts"
label.textAlignment = .left
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1)
view.addSubview(label)
let selectLabel = UILabel(frame: CGRect(x: 0 , y: 0, width: 100, height: 22))
selectLabel.center = CGPointMake(self.tableView.width - 50, label.centerY)
selectLabel.textAlignment = .right
selectLabel.textColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
selectLabel.text = "Select All"
let tap = UITapGestureRecognizer()
tap.addTarget(self, action: #selector(cellSelectTap))
selectLabel.isUserInteractionEnabled = true
selectLabel.addGestureRecognizer(tap)
view.addSubview(selectLabel)
// UI判断
changeHeaderSelectButton(section: section, view: view)
view.headerCallback = {[weak self]data in
guard let self else {return}
if let tempData = self.selectData[String(section)] {
// 如果是数量和元数据数量一样表示已经是选择了全部的状态,则清空下
if tempData.count > 0 && tempData.count == self.dataSourceModel[section].count {
self.selectData[String(section)] = []
}else{
self.selectData[String(section)] = data
}
}else{
self.selectData[String(section)] = data
}
DispatchQueue.main.async {
self.dataChangeCallBack(self.isAllData())
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
}
}
return view return view
} }
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 34 return 34
} }
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 调起系统弹窗
let cell = tableView.cellForRow(at: indexPath) as! CustomContactDupTableViewCell
if let model = cell.model {
if let vc = self.responderViewController() {
ContactManager.callContactWithIdentifier(model, viewController: vc)
}
}
}
// MARK: 响应方法 // MARK: 辅助方法
@objc func cellSelectTap(){ func changeHeaderSelectButton(section:Int,view:CustomDupHeaderView){
DispatchQueue.main.async {
if let tempData = self.selectData[String(section)] {
if tempData.count == self.dataSourceModel[section].count{
// 改变UI
view.subLabel.text = "Deselect All"
}else {
view.subLabel.text = "Select All"
}
}else {
view.subLabel.text = "Select All"
}
}
} }
func saveSelectModel(index:IndexPath){
let tempModel = self.dataSourceModel[index.section][index.row]
if var array = self.selectData[String(index.section)] {
array.append(tempModel)
self.selectData[String(index.section)] = array
}else{
self.selectData[String(index.section)] = [tempModel]
}
if let header = self.tableView.headerView(forSection: index.section) {
changeHeaderSelectButton(section: index.section, view: header as! CustomDupHeaderView)
}
// 改变导航栏的选择按钮状态
self.dataChangeCallBack(isAllData())
}
func removeSelectModel(index:IndexPath){
let tempModel = self.dataSourceModel[index.section][index.row]
if var array = self.selectData[String(index.section)] {
if array.contains(tempModel){
array.removeAll(where: {$0.identifier == tempModel.identifier})
}
self.selectData[String(index.section)] = array
}
if let header = self.tableView.headerView(forSection: index.section) {
changeHeaderSelectButton(section: index.section, view: header as! CustomDupHeaderView)
}
// 改变导航栏的选择按钮状态
self.dataChangeCallBack(isAllData())
}
// 判断当前数量是不是全部
func isAllData()->Bool{
var selectCount = 0
for (_,value) in self.selectData {
selectCount += value.count
}
var allDataCount = 0
for item in self.dataSourceModel {
allDataCount += item.count
}
if selectCount == allDataCount {
return true
}
return false
}
// 是否显示预览按钮
func showPreMergeButton(){
var show : Bool = false
for(_,value) in self.selectData {
// 判断至少有一组且大于2的开始合并
if value.count >= 2{
show = true
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// 如果有且当前是显示的
if show {
if self.preButtonShowStatus == false {
self.preButtonShowStatus = true
UIView.animate(withDuration: 0.1) {
// 更新约束
self.bottomConstraint?.update(offset: -safeHeight)
self.tabBottomConstraint?.update(offset: -safeHeight - 68)
self.layoutIfNeeded()
}
}
}else {
if self.preButtonShowStatus == true {
self.preButtonShowStatus = false
UIView.animate(withDuration: 0.1) {
// 更新约束
self.bottomConstraint?.update(offset: 68)
self.tabBottomConstraint?.update(offset: -safeHeight)
self.layoutIfNeeded()
}
}
}
}
}
private func removeSingalDataInDataSource()-> [String : [ContactModel]]{
// MARK: 辅助方法 var tempDic : [String : [ContactModel]] = [:]
for (key,value) in self.selectData {
if value.count >= 2{
tempDic[key] = value
}
}
return tempDic
}
} }
//
// ContactDupPreNormalView.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import Foundation
class ContactDupPreNormalView : UIView {
static let CONTACT_MERGED = "contact_merged"
static let CONTACT_MERGED_PRE = "contact_merged_pre"
var dataSourceModel : [[ContactModel]] = []
var preButtonShowStatus : Bool = false
// 如果将不想合并的重复联系人删除完了,就显示没有预览可用页面
var dataChangeCallBack : (Bool)->Void = {isClear in}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Duplicates"
label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
label.textAlignment = .left
return label
}()
lazy var subTitleLabel: UILabel = {
let label = UILabel()
label.text = "\(self.dataSourceModel.count) Contacts"
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
label.textAlignment = .left
return label
}()
lazy var tableView : UITableView = {
let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 0, height: 12), style: UITableView.Style.grouped)
tableView.dataSource = self
tableView.delegate = self
tableView.register(CustomContactDupPreTableViewCell.self, forCellReuseIdentifier: "CustomContactDupPreTableViewCell")
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
tableView.showsVerticalScrollIndicator = false
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
return tableView
}()
lazy var mergeButtonView : MergeButtonView = {
let view = MergeButtonView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.titleLabel)
self.addSubview(self.subTitleLabel)
self.addSubview(self.tableView)
self.addSubview(self.mergeButtonView)
self.titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15 * RScreenW())
make.top.equalToSuperview().offset(14 * RScreenH())
make.width.equalTo(345 * RScreenW())
make.height.equalTo(32)
}
self.subTitleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15 * RScreenW())
make.top.equalTo(self.titleLabel.snp.bottom).offset(2 * RScreenH())
make.width.equalTo(345 * RScreenW())
make.height.equalTo(20)
}
self.tableView.snp.makeConstraints { make in
make.top.equalTo(self.subTitleLabel.snp.bottom).offset(16 * RScreenH())
make.left.equalToSuperview().offset(15 * RScreenW())
make.right.equalToSuperview().offset(-15 * RScreenW())
make.bottom.equalToSuperview().offset(-safeHeight - 68)
}
self.mergeButtonView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.bottom.equalToSuperview().offset(-safeHeight)
make.height.equalTo(68)
}
self.mergeButtonView.mergeCallBack = {
ContactManager.mergeContacts(groups: self.dataSourceModel) { result in
switch result {
case .success(let newIDs):
print("新联系人ID: \(newIDs)")
DispatchQueue.main.async {
self.removeData()
// 弹框提示成功
let buAlertVc = ContactBackUpDeleteCompletedAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
buAlertVc.removeFromSuperview()
// 返回重复项
}
self.responderViewController()?.navigationController?.popViewController(animated: true)
}
break
case .failure(let error):
print("合并失败: \(error.localizedDescription)")
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 合并成功后移除数据源
func removeData(){
self.dataSourceModel.removeAll()
self.tableView.reloadData()
// 更新下头部数据
self.subTitleLabel.text = "\(self.dataSourceModel.count) Contacts"
self.dataChangeCallBack(true)
// 发起通知
let dataUpdated = Notification.Name(ContactDupPreNormalView.CONTACT_MERGED)
NotificationCenter.default.post(name: dataUpdated, object: nil, userInfo: nil)
}
}
extension ContactDupPreNormalView : UITableViewDelegate,UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return self.dataSourceModel.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 调起系统弹窗
let cell = tableView.cellForRow(at: indexPath) as! CustomContactDupPreTableViewCell
if let model = cell.model.first {
if let vc = self.responderViewController() {
ContactManager.callContactWithIdentifier(model, viewController: vc)
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomContactDupPreTableViewCell", for: indexPath) as! CustomContactDupPreTableViewCell
cell.indexPath = indexPath
cell.model = self.dataSourceModel[indexPath.section]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 77 + 8 * RScreenH()
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = CustomDupPreHeaderView(frame: CGRect(x: 0 , y: 0, width: self.tableView.width, height: 22))
view.model = self.dataSourceModel[section]
view.currentSection = section
view.headerCallback = { array, currentSection in
self.dataSourceModel.remove(at: currentSection)
DispatchQueue.main.async {
self.tableView.reloadData()
self.subTitleLabel.text = "\(self.dataSourceModel.count) Contacts"
if self.dataSourceModel.count == 0 {
self.dataChangeCallBack(true)
}
}
}
return view
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 34
}
}
//
// ContactNoDupPreView.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import Foundation
class ContactNoDupPreView : UIView {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Duplicates"
label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
label.textAlignment = .left
return label
}()
lazy var subTitleLabel: UILabel = {
let label = UILabel()
label.text = "0 Contacts"
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
label.textAlignment = .left
return label
}()
lazy var tipLabel: UILabel = {
let label = UILabel()
label.text = "No Preview Available"
label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
label.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
label.textAlignment = .center
return label
}()
lazy var subTipLabel: UILabel = {
let label = UILabel()
label.text = "Go back and select duplicate contacts to see a preview"
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
label.textColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.titleLabel)
self.addSubview(self.subTitleLabel)
self.addSubview(self.tipLabel)
self.addSubview(self.subTipLabel)
self.titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15 * RScreenW())
make.top.equalToSuperview().offset(14 * RScreenH())
make.width.equalTo(345 * RScreenW())
make.height.equalTo(32)
}
self.subTitleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15 * RScreenW())
make.top.equalTo(self.titleLabel.snp.bottom).offset(2 * RScreenH())
make.width.equalTo(345 * RScreenW())
make.height.equalTo(20)
}
self.tipLabel.snp.makeConstraints { make in
make.top.equalTo(self.subTitleLabel.snp.bottom).offset(218 * RScreenH())
make.width.equalTo(315 * RScreenW())
make.height.equalTo(34)
make.centerX.equalToSuperview()
}
self.subTipLabel.snp.makeConstraints { make in
make.top.equalTo(self.tipLabel.snp.bottom).offset(8 * RScreenH())
make.width.equalTo(315 * RScreenW())
make.height.equalTo(40)
make.centerX.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//
// MergeButtonView.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import Foundation
import Contacts
class MergeButtonView : UIView {
var mergeCallBack : ()->Void = {}
lazy var mergeButton : UIButton = {
let view = UIButton(type: .custom)
view.backgroundColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
view.setTitleColor(.white, for: .normal)
view.clipsToBounds = true
view.layer.cornerRadius = 23
view.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)
view.setTitle("Merge 0 Contacts", for: .normal)
view.setImage(UIImage(named: "ic_hebing"), for: .normal)
// 设置间距为 8
let spacing: CGFloat = 8
// 获取图片和文字的大小
let imageSize = view.imageView?.image?.size ?? .zero
let titleSize = view.titleLabel?.intrinsicContentSize ?? .zero
// 计算 imageEdgeInsets 和 titleEdgeInsets
view.imageEdgeInsets = UIEdgeInsets(top: 0, left: -spacing / 2, bottom: 0, right: spacing / 2)
view.titleEdgeInsets = UIEdgeInsets(top: 0, left: spacing / 2, bottom: 0, right: -spacing / 2)
view.addTarget(self, action: #selector(merge), for: .touchUpInside)
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .white
self.addSubview(self.mergeButton)
self.mergeButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.height.equalTo(46)
make.top.equalToSuperview().offset(16)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension MergeButtonView {
@objc func merge (){
// 弹出提示框
alertWhenMergeContact()
}
fileprivate func showMeegeAlert(_ self: MergeButtonView) {
// 直接合并
let mergeAlertView : ContactMergeAlertView = ContactMergeAlertView()
mergeAlertView.frame = (self.responderViewController()?.view.bounds)!
cWindow?.addSubview(mergeAlertView)
mergeAlertView.sureCallBack = {
self.mergeCallBack()
}
}
func alertWhenMergeContact() {
// 删除之前弹出是否需要备份
let alertVc = ContactBackupAlertView()
alertVc.frame = (self.responderViewController()?.view.bounds)!
self.responderViewController()?.view.addSubview(alertVc)
alertVc.sureCallBack = {[weak self] isSure in
guard let self else {return}
if isSure {
backupContactsByselect {
// 备份完成后开始合并
self.showMeegeAlert(self)
}
}else{
showMeegeAlert(self)
}
}
}
func backupContactsByselect(success:@escaping()->Void){
// 直接备份所有联系人
let vm = BackupViewModel()
// 获取所有联系人
let store = CNContactStore()
let keysToFetch = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
do {
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
var allContacts : [ContactModel] = []
try store.enumerateContacts(with: request) { contact, stop in
if let model = ContactModel.init(contact: contact) {
allContacts.append(model)
}
}
vm.backupPartialContacts(allContacts) { finised, error in
if finised {
// 备份成功
DispatchQueue.main.async {
let buAlertVc = ContactBackUpCompletedAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
buAlertVc.removeFromSuperview()
success()
}
}
}else{
Print("备份失败")
}
}
} catch {
DispatchQueue.main.async {
print("获取联系人信息时发生错误: \(error)")
}
}
}
}
//
// MergePreButtonView.swift
// PhoneManager
//
// Created by edy on 2025/5/8.
//
import Foundation
class MergePreButtonView : UIView {
var mergePreCallBack : ()->Void = {}
lazy var mergePreButton : UIButton = {
let view = UIButton(type: .custom)
view.backgroundColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
view.setTitleColor(.white, for: .normal)
view.clipsToBounds = true
view.layer.cornerRadius = 23
view.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)
view.setTitle("See Merge Preview", for: .normal)
view.setImage(UIImage(named: "ic_hebing"), for: .normal)
// 设置间距为 8
let spacing: CGFloat = 8
// 获取图片和文字的大小
let imageSize = view.imageView?.image?.size ?? .zero
let titleSize = view.titleLabel?.intrinsicContentSize ?? .zero
// 计算 imageEdgeInsets 和 titleEdgeInsets
view.imageEdgeInsets = UIEdgeInsets(top: 0, left: -spacing / 2, bottom: 0, right: spacing / 2)
view.titleEdgeInsets = UIEdgeInsets(top: 0, left: spacing / 2, bottom: 0, right: -spacing / 2)
view.addTarget(self, action: #selector(mergePre), for: .touchUpInside)
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .white
self.addSubview(self.mergePreButton)
self.mergePreButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.height.equalTo(46)
make.top.equalToSuperview().offset(16)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension MergePreButtonView {
@objc func mergePre (){
self.mergePreCallBack()
}
}
...@@ -6,20 +6,32 @@ ...@@ -6,20 +6,32 @@
// //
import Foundation import Foundation
import SnapKit
class ContactNormalIncomView : UIView { class ContactNormalIncomView : UIView {
static let CONTACT_INCOM = "contact_incom"
private var bottomConstraint: Constraint?
private var tabBottomConstraint: Constraint?
var dataSourceModel : [ContactModel] = [] var dataSourceModel : [ContactModel] = []
var dataClearCallBack : ()->Void = {}
var selectDataChangeCallBack: (Bool)->Void = {show in}
/// 分组后的联系人
private var sectionedContacts: [String: [ContactModel]] = [:]
/// 联系人首字母数组
private var sectionTitles: [String] = []
/// 选择的联系人 /// 选择的联系人
var selectedContacts: [ContactModel] = [] var selectedContacts: [ContactModel] = [] {
didSet{
}
}
var selectedIndex = 0 var selectedIndex = 0
...@@ -42,7 +54,7 @@ class ContactNormalIncomView : UIView { ...@@ -42,7 +54,7 @@ class ContactNormalIncomView : UIView {
}() }()
lazy var tableView : UITableView = { lazy var tableView : UITableView = {
let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 0, height: 12), style: UITableView.Style.grouped) let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 0, height: 12), style: UITableView.Style.plain)
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
tableView.register(CustomContactAllViewTableViewCell.self, forCellReuseIdentifier: "CustomContactAllViewTableViewCell") tableView.register(CustomContactAllViewTableViewCell.self, forCellReuseIdentifier: "CustomContactAllViewTableViewCell")
...@@ -57,10 +69,6 @@ class ContactNormalIncomView : UIView { ...@@ -57,10 +69,6 @@ class ContactNormalIncomView : UIView {
lazy var deleteButton : DeleteButtonView = { lazy var deleteButton : DeleteButtonView = {
let deleteButton = DeleteButtonView() let deleteButton = DeleteButtonView()
// 设置删除按钮
deleteButton.layer.cornerRadius = 23
deleteButton.clipsToBounds = true
deleteButton.isHidden = true
return deleteButton return deleteButton
}() }()
...@@ -89,18 +97,13 @@ class ContactNormalIncomView : UIView { ...@@ -89,18 +97,13 @@ class ContactNormalIncomView : UIView {
make.top.equalTo(self.subTitleLabel.snp.bottom).offset(16 * RScreenH()) make.top.equalTo(self.subTitleLabel.snp.bottom).offset(16 * RScreenH())
make.left.equalToSuperview().offset(15 * RScreenW()) make.left.equalToSuperview().offset(15 * RScreenW())
make.right.equalToSuperview().offset(-15 * RScreenW()) make.right.equalToSuperview().offset(-15 * RScreenW())
make.bottom.equalToSuperview().offset(-102 * RScreenH()) self.tabBottomConstraint = make.bottom.equalToSuperview().offset(-safeHeight).constraint
} }
self.deleteButton.snp.makeConstraints { make in self.deleteButton.snp.makeConstraints { make in
make.top.equalTo(self.tableView.snp.bottom).offset(16 * RScreenH()) make.left.right.equalToSuperview()
make.width.equalTo(345 * RScreenW()) self.bottomConstraint = make.bottom.equalToSuperview().offset(68).constraint
make.height.equalTo(46) make.height.equalTo(68)
make.centerX.equalToSuperview()
} }
// 排序
self.sortContacts()
self.deleteButton.submitCallBack = { self.deleteButton.submitCallBack = {
self.alertWhenDeleteSomeOne() self.alertWhenDeleteSomeOne()
} }
...@@ -112,14 +115,21 @@ class ContactNormalIncomView : UIView { ...@@ -112,14 +115,21 @@ class ContactNormalIncomView : UIView {
} }
extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate { extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sectionTitles.count
return self.dataSourceModel.count
} }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let sectionTitle = sectionTitles[section] // 调起系统弹窗
return sectionedContacts[sectionTitle]?.count ?? 0 let cell = tableView.cellForRow(at: indexPath) as! CustomContactAllViewTableViewCell
if let model = cell.model {
if let vc = self.responderViewController() {
ContactManager.callContactWithIdentifier(model, viewController: vc)
}
}
} }
func updateDeleteButtonStatus() { func updateDeleteButtonStatus() {
...@@ -127,22 +137,30 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate { ...@@ -127,22 +137,30 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate {
// 判断button是否显示 // 判断button是否显示
if self.selectedContacts.count > 0 { if self.selectedContacts.count > 0 {
// 设置button的title // 设置button的title
self.deleteButton.titleLabel.text = "Delete \(self.selectedContacts.count) Contact" self.deleteButton.deleteButton.setTitle("Delete \(self.selectedContacts.count) Contact", for: .normal)
self.deleteButton.isHidden = false UIView.animate(withDuration: 0.1) {
// 更新约束
self.bottomConstraint?.update(offset: -safeHeight)
self.tabBottomConstraint?.update(offset: -safeHeight - 68)
self.layoutIfNeeded()
}
}else{ }else{
UIView.animate(withDuration: 0.1) {
self.deleteButton.isHidden = true // 更新约束
self.bottomConstraint?.update(offset: 68)
self.tabBottomConstraint?.update(offset: -safeHeight)
self.layoutIfNeeded()
}
} }
} }
} }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomContactAllViewTableViewCell", for: indexPath) as! CustomContactAllViewTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "CustomContactAllViewTableViewCell", for: indexPath) as! CustomContactAllViewTableViewCell
let sectionTitle = sectionTitles[indexPath.section] let contact = self.dataSourceModel[indexPath.row]
let contact = sectionedContacts[sectionTitle]?[indexPath.row]
cell.model = contact cell.model = contact
cell.nameLabel.text = contact?.name cell.nameLabel.text = contact.fullName
if self.selectedContacts.contains(where: { $0.identifier == contact!.identifier }) { if self.selectedContacts.contains(where: { $0.identifier == contact.identifier }) {
cell.selectButton.isSelected = true cell.selectButton.isSelected = true
}else { }else {
cell.selectButton.isSelected = false cell.selectButton.isSelected = false
...@@ -154,9 +172,8 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate { ...@@ -154,9 +172,8 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate {
}else{ }else{
self.selectedContacts.removeAll(where: { $0.identifier == model.identifier }) self.selectedContacts.removeAll(where: { $0.identifier == model.identifier })
} }
updateDeleteButtonStatus() updateDeleteButtonStatus()
changeHeaderSelectButtonStatus()
} }
return cell return cell
...@@ -165,25 +182,12 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate { ...@@ -165,25 +182,12 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 77 + 8 * RScreenH() return 77 + 8 * RScreenH()
} }
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel(frame: CGRect(x: 0 , y: 0, width: 345, height: 20))
label.text = sectionTitles[section]
label.textAlignment = .left
label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
label.textColor = UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1)
return label
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 20
}
func alertWhenDeleteSomeOne() { func alertWhenDeleteSomeOne() {
// 删除之前弹出是否真的需要删除 // 删除之前弹出是否真的需要删除
let alertVc = ContactDeleteAlertView() let alertVc = ContactDeleteAlertView()
alertVc.frame = (self.responderViewController()?.view.bounds)! alertVc.frame = (self.responderViewController()?.view.bounds)!
self.responderViewController()?.view.addSubview(alertVc) cWindow?.addSubview(alertVc)
alertVc.sureCallBack = {[weak self] isSure in alertVc.sureCallBack = {[weak self] isSure in
guard let self else {return} guard let self else {return}
...@@ -193,74 +197,49 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate { ...@@ -193,74 +197,49 @@ extension ContactNormalIncomView : UITableViewDataSource,UITableViewDelegate {
} }
} }
func deleteContacts(){ func deleteContacts(){
// 删除逻辑 // 删除逻辑
for contact in selectedContacts { for contact in selectedContacts {
if let index = self.dataSourceModel.firstIndex(of: contact) { if let index = self.dataSourceModel.firstIndex(of: contact) {
self.dataSourceModel.remove(at: index) self.dataSourceModel.remove(at: index)
} }
for (key, var sectionContacts) in sectionedContacts {
if let index = sectionContacts.firstIndex(of: contact) {
sectionContacts.remove(at: index)
sectionedContacts[key] = sectionContacts
}
}
}
selectedContacts.removeAll()
sortContacts()
self.subTitleLabel.text = "\(self.dataSourceModel.count) Contacts"
self.updateDeleteButtonStatus()
self.tableView.reloadData()
// fixme: 需要从联系人列表中删除
// 删除完成 弹窗
let buAlertVc = ContactBackUpDeleteCompletedAlertView(frame: (self.responderViewController()?.view.bounds)!)
self.responderViewController()?.view.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
buAlertVc.removeFromSuperview()
} }
ContactManager.batchDeleteContacts(self.selectedContacts) { result in
switch result {
case .success(let deletedContacts):
} print("成功删除 \(deletedContacts.count) 个联系人")
self.selectedContacts.removeAll()
self.subTitleLabel.text = "\(self.dataSourceModel.count) Contacts"
/// 给联系人分组排序 self.updateDeleteButtonStatus()
func sortContacts() { self.tableView.reloadData()
sectionedContacts.removeAll()
for contact in self.dataSourceModel { if self.dataSourceModel.count <= 0 {
let firstLetter = pinyinFirstLetter(contact.name).uppercased() self.dataClearCallBack()
if sectionedContacts[firstLetter] == nil { }
sectionedContacts[firstLetter] = []
// 删除完成 弹窗
let buAlertVc = ContactBackUpDeleteCompletedAlertView(frame: (cWindow?.bounds)!)
cWindow?.addSubview(buAlertVc)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
buAlertVc.removeFromSuperview()
}
break
case .failure(.contactNotFound(let missing)):
print("操作终止:未找到联系人 \(missing.identifier)")
case .failure(.unauthorized):
print("无通讯录访问权限")
case .failure(.executionFailed(let error)):
print("操作失败:\(error.localizedDescription)")
} }
sectionedContacts[firstLetter]?.append(contact)
} }
sectionTitles = sectionedContacts.keys.sorted()
for key in sectionTitles {
sectionedContacts[key] = sectionedContacts[key]?.sorted {
return pinyinFirstLetter($0.name).uppercased() < pinyinFirstLetter($1.name).uppercased()
}
}
} }
// 获取拼音首字母 func changeHeaderSelectButtonStatus(){
private func pinyinFirstLetter(_ string: String) -> String { if self.dataSourceModel.count != self.selectedContacts.count {
let mutableString = NSMutableString(string: string) self.selectDataChangeCallBack(true)
CFStringTransform(mutableString, nil, kCFStringTransformToLatin, false) }else {
CFStringTransform(mutableString, nil, kCFStringTransformStripDiacritics, false) self.selectDataChangeCallBack(false)
var pinyin = mutableString as String
if pinyin.isEmpty {
return "#"
} }
pinyin = pinyin.replacingOccurrences(of: " ", with: "")
return pinyin.isEmpty ? "#" : String(pinyin.first!).uppercased()
} }
} }
...@@ -67,7 +67,7 @@ class ContactModuleView : UIView { ...@@ -67,7 +67,7 @@ class ContactModuleView : UIView {
make.top.equalTo(self.titleLabel.snp.bottom).offset(18 * RScreenH()) make.top.equalTo(self.titleLabel.snp.bottom).offset(18 * RScreenH())
make.width.equalTo(345 * RScreenW()) make.width.equalTo(345 * RScreenW())
make.centerX.equalToSuperview() make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-34) make.bottom.equalToSuperview().offset(-safeHeight)
} }
} }
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import Foundation import Foundation
import GoogleMobileAds import GoogleMobileAds
import ContactsUI
class Singleton { class Singleton {
// 使用静态常量来保存单例实例 // 使用静态常量来保存单例实例
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment