Principiul Inversării Dependenței în SWIFT


Principiul Inversării Dependenței sau Dependency Inversion Principle afirmă faptul că modulele de pe nivelul ierarhic superior trebuie să fie decuplate de cele de pe nivelurile ierarhice inferioare. Această decuplare între modulele superioare și cele de nivel inferior se va realiza prin introducerea unui nivel tampon de abstractizare între clasele care formează nivelul ierarhic superior și cele care formează nivelurile ierarhice inferioare.
În plus principiul consideră că abstractizarea nu trebuie să depindă de detalii, ci detaliile trebuie sa depindă de abstractizare. Acest principiu este foarte important pentru reutilizarea componentelor software. De asemenea, aplicarea corectă a acestui principiu face ca întreținerea codului să fie mult mai ușor de realizat.

Imediat când citim o astfel de definiție, gândul ne duce la protocoale și la ceea ce înseamnă abstractizarea în SWIFT.

Să intrăm mai mult în detalii.

Avem cazul concret în cadrul aplicației pe care-o dezvoltăm împreună cu elevii din grupul nostru Apple App@DMT.
Construcției ViewController-ul dedicat afisării detaliilor pentru ofertă -> aici

Avem clasele OfferDetailsTableViewCell1 și OfferDetailsTableViewCell2
import UIKit

class OfferDetailsTableViewCell2: UITableViewCell {

    static let ReuseIdentifier = String(describing: OfferDetailsTableViewCell2.self)
    static let NibName = String(describing: OfferDetailsTableViewCell2.self)
    @IBOutlet weak var descriptionLabel: UILabel!
  
}

și

class OfferDetailsTableViewCell1: UITableViewCell {

    static let ReuseIdentifier = String(describing: OfferDetailsTableViewCell1.self)
    static let NibName = String(describing: OfferDetailsTableViewCell1.self)
    @IBOutlet weak var headerLabel: UILabel!
    @IBOutlet weak var dateLabel: UILabel!
    @IBOutlet weak var priceLabel: UILabel!
}

clase acoperitoare pentru doua XIB-uri de UITableViewCell.

Ceea ce m-a făcut să scriu acest articol este modul în care este definit delegatul tableView(tableView:, cellForRowAt indexPath:)

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        switch cellDataArray[indexPath.row].cell {
        case 1:
            let  cell = tableView.dequeueReusableCell(withIdentifier: OfferDetailsTableViewCell1.ReuseIdentifier) as! OfferDetailsTableViewCell1
            cell.headerLabel.text = cellDataArray[indexPath.row].text
            cell.dateLabel.text = "BUBU"
            cell.priceLabel.text = "10 RON"
            return cell
            
        case 2:
             let cell = tableView.dequeueReusableCell(withIdentifier: OfferDetailsTableViewCell2.ReuseIdentifier) as! OfferDetailsTableViewCell2
            cell.descriptionLabel.text = cellDataArray[indexPath.row].text
             return cell

        default:
             let cell = tableView.dequeueReusableCell(withIdentifier: OfferDetailsTableViewCell1.ReuseIdentifier) as! OfferDetailsTableViewCell1
             cell.headerLabel.text = cellDataArray[indexPath.row].text
             cell.dateLabel.text = "BUBU"
             cell.priceLabel.text = "10 RON"
            return cell
        }
    
    }

Vedem că în fiecare case din switch se vor instanția cell-uri și, de asemenea, se vor returna aceste cell-uri.
Orice modificare facută în clasa inferioară, OfferDetailsTableViewCell2 sau OfferDetailsTableViewCell1, trebuie de asemenea sa se opereze în clasele de nivel superior, în cazul nostru OfferDtailsViewController care implementează UITableViewDelegate și UITableViewDataSource.

Cum rezolvăm această cuplare?

1. Introducem un protocol în care declarăm o functie config(withData:)

protocol OffersTableViewCellProtocol {
    func config(withData:Any)
}

2. Acest protocol este adoptat de fiecare dintre clasele OfferDetailsTableViewCell1 și OfferDetailsTableViewCell2

class OfferDetailsTableViewCell1: UITableViewCell, OffersTableViewCellProtocol {

    func config(withData: Any) {
        let data = withData as! cellData
        self.headerLabel.text = data.text
        self.dateLabel.text = "BUBU"
        self.priceLabel.text = "10 RON"
    }
    
    static let ReuseIdentifier = String(describing: OfferDetailsTableViewCell1.self)
    static let NibName = String(describing: OfferDetailsTableViewCell1.self)
    
    @IBOutlet weak var headerLabel: UILabel!
    @IBOutlet weak var dateLabel: UILabel!
    @IBOutlet weak var priceLabel: UILabel!
}

și

class OfferDetailsTableViewCell2: UITableViewCell, OffersTableViewCellProtocol {
    
    func config(withData: Any) {
        let data = withData as! cellData
        self.descriptionLabel.text = data.text
    }

    static let ReuseIdentifier = String(describing: OfferDetailsTableViewCell2.self)
    static let NibName = String(describing: OfferDetailsTableViewCell2.self)
    
    @IBOutlet weak var descriptionLabel: UILabel!
    
}

3. Rescriem delegatul tableView(tableView:, cellForRowAt indexPath:)

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        var cell:OffersTableViewCellProtocol? = nil
        
        switch cellDataArray[indexPath.row].cell {
        case 1:
                cell = tableView.dequeueReusableCell(withIdentifier: OfferDetailsTableViewCell1.ReuseIdentifier) as? OffersTableViewCellProtocol
                cell?.config(withData: cellDataArray[indexPath.row])
            
        case 2:
                cell = tableView.dequeueReusableCell(withIdentifier: OfferDetailsTableViewCell2.ReuseIdentifier) as? OffersTableViewCellProtocol
                cell?.config(withData: cellDataArray[indexPath.row])

        default:
            cell = tableView.dequeueReusableCell(withIdentifier: OfferDetailsTableViewCell1.ReuseIdentifier) as? OffersTableViewCellProtocol
                cell?.config(withData: cellDataArray[indexPath.row])
        }

        return cell as! UITableViewCell
    }

Prin intermediul protocolului OffersTableViewCellProtocol am reușit să punem în aplicare principiul inversării dependenței astfel încăt decuplarea dintre cele nivele să fie funcțională.

Share Button

De ce @objc în SWIFT 4?

Atunci când adăugăm o clauză @objc unei metode în Swift, aceasta din urmă devine accesibilă din Objective C. Bun, teoretic sună foarte riguros, dar dacă eu nu am nici un fel de cod Objective C în aplicație? Ar trebui să nu am nici o problemă, nu?

Swift 4 folosește încă în fundal foarte multe protocoale ObjectiveC.


UITableViewDataSource in ObjC

Ar trebui de fiecare dată când implementăm metodele din UITableViewDataSource să le specificăm fiecăreia un @objc. Swift 4 are grijă și nu ne forțează în acest sens.

Bun, și atunci unde și de ce am nevoie de @objc în cod Swift 4?

Cel mai bun exemplu în acest sens este o metodă care folosește un #selector. Avem nevoie să executăm o metodă loadRecordings după un delay de 0.5 secunde în thread-ul curent.

Pentru aceasta folosesc perform(_:with:afterDelay:), unde primul argument intern este un Selector, un struct ObjectiveC Runtime.

    perform(#selector(loadRecordings), with: nil, afterDelay: 0.5)

Deci selectorul din perform va trebui sa fie vizibil în runtime direct în ObjectiveC. Deci ar trebui ca metoda loadRecordings să aibe specificată o clauză @objc.
Deci prin deducție inversă, am ajuns la concluzia că trebuie să definesc astfel metoda loadRecordings

@objc func loadRecordings() {
.....
    }

Lipsa acestei clauze @objc îmi va genera o eroare la compilare așa cum se vede și mai jos:

Share Button

Swift 4 – Cât de util este closure în UiAlertController?

UIAlertController

Vorbeam acum ceva timp despre closure într-un context mai didactic. De curând am avut nevoie pentru a oferi o explicație despre closure într-un context ceva mai amplu.

Cadrul în care se desfășoară activitatea noastră este următorul:

 

Sunt într-un ViewController – VC1 – și apăsând pe un Button (pasul 1) ajungem într-un al doilea ViewControoler – VC2. În acest al doilea VC trebuie sa completăm câteva detalii să zicem – nume și email și să le aruncăm peste rețea (pasul 2). Serverul procesează ceea ce i-am trimis noi și ne răspunde (pasul 3) cu un mesaj de “succes“.

În momentul în primim mesajul de “succes” deschidem un UIAlertController (pasul 4) care imi arată mesajul primit. La apăsarea butonului de OK (pasul 5) trebuie sa eliberăm VC2 și să ne întoarcem în VC1.

Ce problemă avem la pasul 5? Operațiunea de eliberare a VC2 trebuie făcută în momentul în care se face click pe OK, nu în momentul în care se afisează UIAlertController-ul.

Funcția care face afișarea UIAlertController-ului este definită într-un Alertmanager astfel:

static func showGenericDialog(_ message: String, viewController: UIViewController, completionHandler: @escaping () -> Void ) {
        
        let alert = UIAlertController(title: "TITLE", message: message, preferredStyle: UIAlertControllerStyle.alert)
        let okAction: UIAlertAction = UIAlertAction(title: "Ok", style: .default, handler: { (action: UIAlertAction) in
            completionHandler()
            
        })
        alert.addAction(okAction)
        OperationQueue.main.addOperation {
            viewController.present(alert, animated: true, completion: nil)
        }
        
    }

Ce se observă? Am definit în okAction un UIAlertAction cu un handler care se va “activa” atunci când se apasă pe okAction. Acțiunea din handler, funcția anonimă prezentă acolo va fi implementată în momentul în care această funcție este apelată.

Prin urmare, avem un minunat closure în momentul în care vom apela funcția showGenericDialog:

AlertManager.showGenericDialog(message, viewController: self!,completionHandler: {
                            self?.dismiss(animated: true, completion: nil)
                        })

Avem prin urmare cu ajutorul funcției anonime trecerea de la VC2 la VC1 atunci când utilizatorul face click pe butonul de OK.

Share Button

XCODE 9 – Main Thread Checker

Tocmai ce am receptționat următoarea avertizare în XCode:

XCode- Main Thread only

si am considerat oportun momentul de a explica un pic ce înseamnă aceste avertizări noi de culoare violet care apar în XCode.

În XCode 9, Apple aduce un nou tool numit Main Thread Checker, specificând faptul că “Main Thread Checker Detects invalid use of AppKit, UIKit, and other APIs from a background thread.

Să detaliem un pic acest nou tool din XCode 9. Apple a proiectat câteva din framework-urile sale pentru a funcționa doar în main thread, utilizarea acestora în background thread fiind foarte delicată. Mai pe românește va îngheța ecranul aplicației. Dacă până la versiunea 9 a XCode-ului această problemă trebuia gestionată exclusiv de developer, de la versiunea 9 în sus, XCode-ul ne dă un mic bobărnac în acest sens.

Exemplul celor de la Apple este similar cu problema pe care o am eu:

 

let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
   if let data = data {      
      self.label.text = "\(data.count) bytes downloaded"
      // Error: label updated on background thread   
   }
}
task.resume()

Ideea este că vreau să modific o etichetă în UI asteptând după completion handler. Dacă răspunsul de la server întârzie, implicit voi avea o problemă cu UI deoarece ea nu este funcțională decât după ce termin completion handler-ul. Acțiunea de networking din background îmi provoacă probleme!

Main Thread Checker din XCode 9 vine tocmai cu o mică avertizare în acest sens:  runtime: UI API called from background thread: UILabel.text must be used from main thread only, sugerându-ne și ceea ce trebuie făcut: să mutăm în main thread acțiunea de UI, fără a compromite integritatea aplicației:

 

let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
   if let data = data {    
      DispatchQueue.main.async {  
      self.label.text = "\(data.count) bytes downloaded"
      }
   }
}
task.resume()

 

Concluzia este următoarea: dacă avem activități de UI care asteaptă un mesaj din o activitate de background thread, e recomandată trecerea activității de UI în main thread folosind DispatchQueue.main.async

Share Button

XCode nu suportă versiunea actuală de IOS. Ce este de făcut?

Tocmai ce-am făcut update la ultima versiune de IOS – 11.3 (15E216) și bineînțeles că versiunea mea de XCode – 9.2 (9C40b) nu mai îmi poate “vedea” telefonul pentru teste.Din ce motiv se întâmplă acest lucru?

Ori de câte ori este lansată o nouă versiune semnificativă de iOS, Apple lansează și o nouă versiune de XCode. La sfârșitul lunii martie 2018 a fost lansată versiunea 11.3 de IOS simultan cu XCode 9.3, prin urmare dacă sunt developer și îmi actualizez telefonul la ultima versiune de IOS, musai trebuie să actualizez și XCode-ul. Însă developer-ii nu-și actualizează XCode-ul imediat după apariția sa, acest lucru însemnând, de cele mai multe ori, upgrade de proiect.
Revenind, nouă versiune de XCode – 9.3 conține DeveloperDiskImage, un folder având numele versiunii, în cazul nostru – 11.3 (15E216), necesară pentru a rula proiectele pe cele mai recente versiuni de iOS. Prin urmare, pentru a rula o aplicație pe un IOS 11.3 ar trebui neapărat să actualizăm XCode-ul la 9.3.

Cum putem ieși din acest impas?
Momentan, țin XCode-ul la 9.2 fiind limitat de faptul că 9.3 îmi cere și upgrade la sistemul de operare, fiind necesar High Sierra. Soluția utilizată în aceste situații este folosirea unui disk DeveloperDiskImage de IOS 11.3 de la un XCode 9.3 pe care să-l importăm în Xcode-ul nostru 9.2. Practic, este un folder care va ști să “asculte” device-urile cu IOS 11.3 pe versiuni inferioare de XCode.

Această arhivă conține DeveloperDiskImage de IOS – 11.3 (15E216). Conținutul arhivei, folderul numit 11.3 (15E216) trebuie copiat în /Application/XCode/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/. Restart la XCode-ul nostru și acum putem rula aplicațiile noastre din XCode 9.2 în IOS 11.3

Share Button