@@ -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-
239236import UIKit
240237
241238@available ( iOS 14 . 0 , * )
242239class 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