Skip to content

Commit 2da1d95

Browse files
committed
custom list contact best
1 parent 46b1741 commit 2da1d95

File tree

1 file changed

+159
-129
lines changed

1 file changed

+159
-129
lines changed

ios/Plugin/ContactsPlugin.swift

Lines changed: 159 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -233,162 +233,192 @@ public class ContactsPlugin: CAPPlugin, CNContactPickerDelegate {
233233

234234
}
235235

236-
// MARK: - UI Customizada para exibir contatos limitados
237-
// Essa classe cria uma interface simples com uma lista (table view) para que o usuário selecione
238-
239236
import UIKit
240237

241238
@available(iOS 14.0, *)
242239
class LimitedContactPickerViewController: UITableViewController {
243-
// Array de contatos (modelo ContactPayload)
240+
244241
var contacts: [ContactPayload] = []
245-
// Callback para retornar o contato selecionado
242+
private var sortedContacts: [String: [ContactPayload]] = [:]
243+
private var sectionTitles: [String] = []
244+
246245
var selectionHandler: ((ContactPayload) -> Void)?
246+
247+
override func viewDidLoad() {
248+
super.viewDidLoad()
249+
250+
self.title = "Limited Contacts" // Forçar título no header
251+
252+
// 🔹 Garante que a barra de navegação apareça corretamente
253+
navigationController?.navigationBar.prefersLargeTitles = false
254+
navigationItem.largeTitleDisplayMode = .never
255+
256+
let closeButton = UIButton(type: .system)
257+
closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
258+
closeButton.tintColor = .systemGray
259+
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
260+
261+
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: closeButton)
262+
263+
tableView.register(ContactTableViewCell.self, forCellReuseIdentifier: "contactCell")
264+
tableView.backgroundColor = .systemGroupedBackground
265+
tableView.rowHeight = 60
266+
267+
sortContacts()
268+
}
247269

248-
override func viewDidLoad() {
249-
super.viewDidLoad()
250-
251-
// Registro da célula padrão
252-
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "contactCell")
253-
254-
// Define o título com suporte a multilíngua
255-
self.title = NSLocalizedString("Lista de contatos permitidos", comment: "Título da lista de contatos permitidos")
256-
257-
// Adiciona um botão fechar (ícone) no canto superior direito
258-
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
259-
image: UIImage(systemName: "xmark"),
260-
style: .plain,
261-
target: self,
262-
action: #selector(closeTapped)
263-
)
264-
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
265-
266-
// Adiciona um cabeçalho vazio para dar espaçamento no topo (por exemplo, 20 pontos)
267-
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 20))
268-
headerView.backgroundColor = .clear
269-
self.tableView.tableHeaderView = headerView
270-
}
271270

272-
@objc func closeTapped() {
273-
self.dismiss(animated: true, completion: nil)
271+
@objc private func closeTapped() {
272+
dismiss(animated: true)
274273
}
275274

276-
// Número de linhas = número de contatos
277-
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
278-
return contacts.count
275+
// MARK: - Organização por letras A-Z
276+
private func sortContacts() {
277+
sortedContacts = Dictionary(grouping: contacts) { contact in
278+
String(contact.contactDisplayName?.prefix(1) ?? "#").uppercased()
279+
}
280+
sectionTitles = sortedContacts.keys.sorted()
279281
}
280282

281-
// Configuração da célula com foto, texto e layout ajustado
282-
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
283-
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath)
284-
let contact = contacts[indexPath.row]
285-
286-
// Configuração padrão do conteúdo da célula
287-
var content = cell.defaultContentConfiguration()
288-
289-
// Define o texto de exibição:
290-
// 1. Se houver nome (contactDisplayName), usa-o.
291-
// 2. Senão, se houver email, usa o primeiro email.
292-
// 3. Senão, se houver telefone, usa o primeiro telefone.
293-
// 4. Caso contrário, exibe "Sem dados".
294-
var displayText = ""
295-
if let name = contact.contactDisplayName, !name.isEmpty {
296-
displayText = name
297-
} else if let email = contact.firstEmail, !email.isEmpty {
298-
displayText = email
299-
} else if let phone = contact.firstPhone, !phone.isEmpty {
300-
displayText = phone
301-
} else {
302-
displayText = NSLocalizedString("Sem dados", comment: "Nenhuma informação disponível")
283+
override func numberOfSections(in tableView: UITableView) -> Int {
284+
return sectionTitles.count
303285
}
304-
content.text = displayText
305286

306-
// Configura a imagem à esquerda:
307-
let jsObject = contact.getJSObject()
308-
print("JSObject para o contato \(contact.contactId): \(jsObject)")
309-
if let imageDict = jsObject["image"] as? [String: Any],
310-
let base64String = imageDict["base64String"] as? String {
311-
print("Base64 String encontrada para o contato \(contact.contactId): \(base64String)")
312-
let base64DataString = base64String.components(separatedBy: ",").last ?? ""
313-
if let imageData = Data(base64Encoded: base64DataString),
314-
let image = UIImage(data: imageData) {
315-
print("Imagem decodificada com sucesso para o contato \(contact.contactId)")
316-
content.image = image
317-
} else {
318-
print("Erro ao criar UIImage para o contato \(contact.contactId)")
319-
content.image = UIImage(systemName: "person.circle")
320-
}
321-
} else {
322-
print("Campo 'image' inválido ou ausente para o contato \(contact.contactId)")
323-
content.image = UIImage(systemName: "person.circle")
287+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
288+
return sortedContacts[sectionTitles[section]]?.count ?? 0
324289
}
325290

326-
// Ajusta o tamanho e o estilo da imagem
327-
content.imageProperties.maximumSize = CGSize(width: 30, height: 30)
328-
content.imageProperties.cornerRadius = 20
291+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
292+
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactTableViewCell
293+
if let contact = sortedContacts[sectionTitles[indexPath.section]]?[indexPath.row] {
294+
cell.configure(with: contact)
295+
}
296+
return cell
297+
}
329298

330-
// Aplica o conteúdo configurado à célula
331-
cell.contentConfiguration = content
299+
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
300+
tableView.deselectRow(at: indexPath, animated: true)
301+
if let selectedContact = sortedContacts[sectionTitles[indexPath.section]]?[indexPath.row] {
302+
selectionHandler?(selectedContact)
303+
dismiss(animated: true)
304+
}
305+
}
332306

333-
// Remove o accessoryType
334-
cell.accessoryType = .none
307+
// 🚫 Removendo índice lateral (letrinhas azuis)
308+
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
309+
return nil
310+
}
335311

336-
return cell
337-
}
338-
339-
// Ao selecionar um contato, chama o callback e fecha a tela
340-
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
341-
let selectedContact = contacts[indexPath.row]
342-
selectionHandler?(selectedContact)
343-
self.dismiss(animated: true, completion: nil)
312+
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
313+
return sectionTitles[section]
344314
}
345315
}
346316

347-
extension ContactPayload {
348-
/// Retorna o nome formatado do contato.
349-
var contactDisplayName: String? {
350-
if let nameDict = self.getJSObject()["name"] as? [String: Any] {
351-
// Tenta utilizar o displayName
352-
if let display = nameDict["display"] as? String, !display.isEmpty {
353-
return display
354-
} else {
355-
// Se não houver display, concatena os componentes disponíveis
356-
var components = [String]()
357-
if let given = nameDict["given"] as? String, !given.isEmpty {
358-
components.append(given)
359-
}
360-
if let middle = nameDict["middle"] as? String, !middle.isEmpty {
361-
components.append(middle)
362-
}
363-
if let family = nameDict["family"] as? String, !family.isEmpty {
364-
components.append(family)
365-
}
366-
if !components.isEmpty {
367-
return components.joined(separator: " ")
368-
}
369-
}
370-
}
371-
return nil
317+
// MARK: - Célula de Contato
318+
@available(iOS 14.0, *)
319+
class ContactTableViewCell: UITableViewCell {
320+
321+
private let contactImageView: UIImageView = {
322+
let imageView = UIImageView()
323+
imageView.layer.masksToBounds = true
324+
imageView.contentMode = .scaleAspectFill
325+
imageView.translatesAutoresizingMaskIntoConstraints = false
326+
return imageView
327+
}()
328+
329+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
330+
super.init(style: .default, reuseIdentifier: reuseIdentifier)
331+
accessoryType = .disclosureIndicator
332+
setupUI()
333+
}
334+
335+
required init?(coder: NSCoder) {
336+
fatalError("init(coder:) has not been implemented")
372337
}
373338

374-
/// Retorna o primeiro email, se disponível.
375-
var firstEmail: String? {
376-
if let emailsArray = self.getJSObject()["emails"] as? [Any],
377-
let firstEmailObj = emailsArray.first as? [String: Any],
378-
let email = firstEmailObj["address"] as? String,
379-
!email.isEmpty {
380-
return email
339+
private func setupUI() {
340+
addSubview(contactImageView)
341+
NSLayoutConstraint.activate([
342+
contactImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
343+
contactImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
344+
contactImageView.widthAnchor.constraint(equalToConstant: 44),
345+
contactImageView.heightAnchor.constraint(equalToConstant: 44)
346+
])
347+
348+
// 🛠 Garante que a imagem fique redonda corretamente
349+
DispatchQueue.main.async {
350+
self.contactImageView.layer.cornerRadius = self.contactImageView.frame.height / 2
381351
}
382-
return nil
383352
}
384353

385-
/// Retorna o primeiro telefone, se disponível.
386-
var firstPhone: String? {
387-
if let phonesArray = self.getJSObject()["phones"] as? [Any],
388-
let firstPhoneObj = phonesArray.first as? [String: Any],
389-
let phone = firstPhoneObj["number"] as? String,
390-
!phone.isEmpty {
391-
return phone
354+
func configure(with contact: ContactPayload) {
355+
var content = defaultContentConfiguration()
356+
357+
content.text = contact.contactDisplayName ?? "Sem Nome"
358+
content.secondaryText = contact.firstPhone ?? contact.firstEmail ?? "Sem Informações"
359+
content.image = contact.loadImage() ?? UIImage(systemName: "person.circle.fill")
360+
361+
// 📸 Melhorando imagem para ficar circular
362+
content.imageProperties.maximumSize = CGSize(width: 44, height: 44)
363+
content.imageProperties.cornerRadius = 22
364+
365+
// 🔠 Nome com peso maior (semibold)
366+
content.textProperties.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
367+
368+
contentConfiguration = content
369+
}
370+
}
371+
372+
// MARK: - Extensão para carregar imagem
373+
extension ContactPayload {
374+
var contactDisplayName: String? {
375+
if let nameDict = self.getJSObject()["name"] as? [String: Any] {
376+
if let display = nameDict["display"] as? String, !display.isEmpty {
377+
return display
378+
} else {
379+
var components = [String]()
380+
if let given = nameDict["given"] as? String, !given.isEmpty {
381+
components.append(given)
382+
}
383+
if let middle = nameDict["middle"] as? String, !middle.isEmpty {
384+
components.append(middle)
385+
}
386+
if let family = nameDict["family"] as? String, !family.isEmpty {
387+
components.append(family)
388+
}
389+
if !components.isEmpty {
390+
return components.joined(separator: " ")
391+
}
392+
}
393+
}
394+
return nil
395+
}
396+
397+
var firstPhone: String? {
398+
if let phonesArray = self.getJSObject()["phones"] as? [[String: Any]],
399+
let firstPhoneObj = phonesArray.first,
400+
let phone = firstPhoneObj["number"] as? String,
401+
!phone.isEmpty {
402+
return phone
403+
}
404+
return nil
405+
}
406+
407+
var firstEmail: String? {
408+
if let emailsArray = self.getJSObject()["emails"] as? [[String: Any]],
409+
let firstEmailObj = emailsArray.first,
410+
let email = firstEmailObj["address"] as? String,
411+
!email.isEmpty {
412+
return email
413+
}
414+
return nil
415+
}
416+
func loadImage() -> UIImage? {
417+
if let imageDict = getJSObject()["image"] as? [String: Any],
418+
let base64String = imageDict["base64String"] as? String,
419+
let imageData = Data(base64Encoded: base64String.components(separatedBy: ",").last ?? ""),
420+
let image = UIImage(data: imageData) {
421+
return image
392422
}
393423
return nil
394424
}

0 commit comments

Comments
 (0)