Πώς να δημιουργήσετε μια όμορφη, επαναχρησιμοποιήσιμη προβολή κλίσης στο Swift με IBDesignable

Αυτό το σεμινάριο θα σας δείξει πώς να δημιουργήσετε μια ευέλικτη, @IBDesignable βαθμίδα προβολής βαθμίδων στο Swift 4. Μπορείτε να αφήσετε το CAGradientView σε πίνακες ιστορικού και να κάνετε προεπισκόπηση στην ώρα του σχεδιασμού. Ή να το προσθέσετε προγραμματικά. Μπορείτε να ορίσετε τα χρώματα για δύο στάσεις κλίσης (αρχή και τέλος) και κατεύθυνση κλίσης (σε μοίρες). Αυτές οι ιδιότητες είναι πλήρως ελεγχόμενες από τον επιθεωρητή ΙΒ.

Γιατί χρειάζεστε αυτό

Οι σχεδιαστές αγαπούν τις κλίσεις. Είναι βέβαιο ότι, όπως τα θολά υπόβαθρα και οι σκιές, μπαίνουν μέσα και έξω από τη μόδα με τον μεταβαλλόμενο άνεμο. Τους τείνουν να είναι πιο λεπτές στις μέρες μας, γι 'αυτό χρειάζονται ένα δίκαιο ποσό για να τους πάρει ακριβώς το δικαίωμα.

Η δημιουργία μιας διαβάθμισης συνεπάγεται ένα εύλογο έργο. Τελειοποίηση μέχρι ο σχεδιαστής σας είναι ευτυχής μπορεί να είναι μια χρονοβόρα διαδικασία. Αυτό το σεμινάριο σας δείχνει πώς μπορείτε να δημιουργήσετε μια συνιστώσα προβολής κλίσης που μπορείτε να βάλετε σε storyboard και να κάνετε προεπισκόπηση από το Interface Builder.

Οι σχεδιαστές σας θα σας αγαπούν γι 'αυτό και θα εξοικονομήσετε πολύ χρόνο.

Τι θα χτίσετε

Είναι εύκολο να πείτε ότι πρόκειται να δημιουργήσετε μια κλίση, αλλά ποιες είναι οι ακριβείς απαιτήσεις; Ας τους ορίσουμε:

  • Πρέπει να είναι μια υποκατηγορία UIView
  • Πρέπει να είναι @ IBDesignable έτσι ώστε να μπορεί να προεπισκόπησης στο Xcode / Interface Builder
  • Πρέπει να είναι πλήρως διαμορφωμένο είτε σε κώδικα είτε σε Interface Builder

Λάβετε το παράδειγμα έργου

Για να ακολουθήσετε σωστά αυτό το σεμινάριο, θα χρειαστείτε το παράδειγμα έργου που μπορείτε να τραβήξετε από το GitHub.

Όταν φορτώνετε το έργο σε Xcode και ανοίγετε το παράδειγμα της σκηνής ViewController στον storyboard, θα μπορείτε να επιλέξετε την προβολή κλίσης και να την επεξεργαστείτε στον Inspector Attributes, όπως φαίνεται στην παρακάτω εικόνα.

Σχετικά με τα επίπεδα κλίσης

ΣΗΜΕΙΩΣΗ: Δεν πρόκειται να εισαχθεί στην CAGradientLayer. Εάν χρειάζεστε μια πιο βασική εισαγωγή, παρακαλώ διαβάστε το Mastering CAGradientLayer στο Swift tutorial που εξηγεί όλο το φόντο που χρειάζεστε.

Υπάρχουν διάφοροι τρόποι για να επιτύχετε ένα εφέ διαβάθμισης στο iOS, αλλά σε αυτό το σεμινάριο θα χρησιμοποιήσουμε το CAGradientLayer. Αυτή είναι μια υποκατηγορία του CALayer, ενός αντικειμένου Core Animation που είναι μέρος της ιεραρχίας του στρώματος της προβολής. Στο iOS, τα UIViews περιγράφονται ως προβολές που υποστηρίζονται από στρώματα επειδή η εμφάνισή τους ελέγχεται από την ιδιότητα στρώματός τους. Κάθε προβολή έχει ένα στρώμα και, όπως κάθε UIView μπορεί να έχει πολλαπλές υποεπεξεργασίες, κάθε στρώμα μπορεί να έχει πολλαπλά υποστυλώματα.

Αυτό σημαίνει στην πράξη ότι κάθε άποψη μπορεί να έχει ένα αυθαίρετα πολύπλοκο δέντρο των στρώσεων για να προσθέσει οπτική πολυπλοκότητα στην άποψη. Όταν εργάζεστε βαριά με το Core Animation, σε κάποιο σημείο ο προγραμματιστής πρέπει να κάνει τη διάκριση ανάμεσα στην προσθήκη πολυπλοκότητας στο επίπεδο CALayer και στην απλή προσθήκη μιας νέας UIView για να επιτύχει το ίδιο αποτέλεσμα. Συνήθως, η οριοθέτηση μεταξύ απόψεων και στρώσεων είναι αρκετά προφανής. Συχνά επειδή απαιτείται κάποια ιδιότητα μιας προβολής για τη λειτουργικότητα της εφαρμογής (για παράδειγμα, απαιτείται UILabel ή UIButton).

Όταν δημιουργούμε πλούσιο περιβάλλον χρήστη με πολλά λεπτές γραφικές παραστάσεις, μπορεί να γίνει πολύ εύκολο να αυξηθεί η πολυπλοκότητα της ιεραρχίας των επιπέδων. Σε γενικές γραμμές, θα πρέπει να αποφύγετε αυτό, επειδή τα επίπεδα μπορούν να διαχειριστούν μόνο σε κώδικα, όχι σε ιστορίες. Η λογική για τη διαχείριση της ιεραρχίας των επιπέδων μπορεί να γίνει αρκετά δυσκίνητη.

Για τους σκοπούς αυτού του σεμιναρίου, θα προσθέσετε ένα CAGradientLayer ως υποεπίπεδο στην ιδιότητα στρώματος της προβολής. Αυτό δίνει μια χαρτογράφηση ενός προς ένα μεταξύ των προβολών και των επιπέδων. Καλύπτει όμορφα κάθε στρώση κλίσης μέσα σε ένα UIView, ώστε να μπορεί να τοποθετηθεί σε ένα storyboard.

Ορισμός της υποκλάσης προβολής

Ο πυρήνας αυτού του εκπαιδευτικού είναι μια προβολή διαβάθμισης που ονομάζεται LDGradientView. Είναι μια υποκατηγορία της UIView και ορίζεται ως εξής:

@IBDesignable class LDGradientView: UIView {
  // ...
}}

Η κλάση έχει επισημανθεί ως @IBDesignable, που σημαίνει ότι μπορεί να προεπισκόπησης στο Interface Builder (επεξεργαστής storyboard του Xcode).

Η ίδια η κλίση ορίζεται ως ιδιωτική ιδιοκτησία της κατηγορίας:

// το στρώμα κλίσης
ιδιωτική κλίμακα var: CAGradientLayer;

Αυτή η ιδιότητα δημιουργείται από τη λειτουργία που ακολουθεί. Ρυθμίζει την ιδιότητα πλαισίου της κλίσης στα όρια της προβολής, οπότε καταλαμβάνει ολόκληρη την προβολή. Αυτό συμβαδίζει με τη χαρτογράφηση one-to-one μεταξύ προβολής και στρώματος.

// δημιουργία στρώματος κλίσης
ιδιωτική λειτουργία createGradient () -> CAGradientLayer {
  let κλίση = CAGradientLayer ()
  gradient.frame = self.bounds
  επιστροφή κλίση
}}

Στη συνέχεια, προστίθεται ως subview του στρώματος της προβολής όπως φαίνεται:

// Δημιουργήστε μια κλίση και εγκαταστήστε την στο στρώμα
ιδιωτικό λειτουργικό σύστημα installGradient () {
  // εάν υπάρχει ήδη μια βαθμίδα που έχει εγκατασταθεί στο στρώμα, αφαιρέστε την
  αν ας gradient = self.gradient {
    gradient.removeFromSuperlayer ()
  }}
  let gradient = createGradient ()
  self.layer.addSublayer (κλίση)
  self.gradient = κλίση
}}

Αυτές είναι και οι δύο ιδιωτικές λειτουργίες επειδή η ιεραρχία στρώματος μιας προβολής πρέπει να είναι δική της επιχείρηση.

Εάν εγκαθιστάτε την προβολή κλίσης σε μια πολύπλοκη ιεραρχία ή οποιαδήποτε εποπτεία που χρησιμοποιεί περιορισμούς, τότε κάθε φορά που έχει οριστεί το πλαίσιο, η προβολή πρέπει να ενημερωθεί η ίδια. Μπορείτε να το κάνετε προσθέτοντας αυτές τις μεθόδους:

Επαναφορά πλαισίου var: CGRect {
  didSet {
    updateGradient ()
  }}
}}
override func layoutSubviews () {
  super.layoutSubviews ()
  // αυτό είναι ζωτικής σημασίας όταν χρησιμοποιούνται περιορισμοί στις εποπτικές εξετάσεις
  updateGradient ()
}}
// Ενημέρωση υπάρχουσας κλίσης
    ιδιωτική λειτουργία updateGradient () {
        αν ας gradient = self.gradient {
            αφήστε startColor = self.startColor; UICcolor.clear
            αφήστε endColor = self.endColor; UICcolor.clear
            gradient.colors = [startColor.cgColor, endColor.cgColor]
            ας (αρχή, τέλος) = gradientPointsForAngle (self.angle)
            gradient.startPoint = έναρξη
            gradient.endPoint = τέλος
            gradient.frame = self.bounds
        }}
    }}

Τέλος, χρειαζόμαστε επίσης κάποιο τρόπο δημιουργίας στιγμιότυπων της προβολής και κλήσης της συνάρτησης installGradient. Αυτό το κάνουμε από ένα από τα δύο initializers, το πρώτο που αρχικοποιείται από το Interface Builder, και το δεύτερο από τα προγραμματιστικά instantization:

// αρχικοποιητές
απαιτούνται init (κωδικοποιητής aDecoder: NSCoder) {
  super.init (κωδικοποιητής: aDecoder)
  installGradient ()
}}
override init (πλαίσιο: CGRect) {
  super.init (πλαίσιο: πλαίσιο)
  installGradient ()
}}

Καθορισμός κλίσης

Τώρα έχετε μια υποκατηγορία UIView που μπορεί να εγκαταστήσει ένα CAGradientLayer, αλλά αυτό δεν επιτυγχάνει πολλά. Ας κάνουμε την προβολή κλίσης να δουλεύει για εμάς ...

Υπάρχουν δύο κύριες ιδιότητες του CAGradientLayer ότι η προσαρμοσμένη προβολή σας θα χειρίζεται. Αυτά είναι:

  • Τα χρώματα της κλίσης
  • Η κατεύθυνση της κλίσης

Ορισμός των χρωμάτων

Τα χρώματα ορίζονται ως ιδιότητα στο CAGradientLayer:

// Μια σειρά αντικειμένων CGColorRef που καθορίζουν το χρώμα κάθε στάσης κλίσης. Ζωντανό.
χρώματα var: [Οποιαδήποτε];

Μια σημείωση σχετικά με τη βαθμίδα σταματά

Τα σημεία στα οποία το χρώμα αλλάζει σε κλίση ονομάζονται στάσεις κλίσης. Οι κλίσεις υποστηρίζουν αρκετά περίπλοκη συμπεριφορά και μπορούν να έχουν απεριόριστες στάσεις. Ο προγραμματισμός αυτής της συμπεριφοράς είναι απλός. Ωστόσο, η δημιουργία μιας διεπαφής @IBInspectable για αυτό, είναι πιο δύσκολη.

Θα ήταν σχετικά ασήμαντο να προσθέσετε άλλο ένα ή δύο σταμάτημα. Η επίλυση του γενικού προβλήματος ενός αυθαίρετου αριθμού στάσεων είναι πιο δύσκολη. Η λύση θα ήταν πιθανώς λιγότερο χρήσιμη από την εκτέλεση της ίδιας εργασίας απευθείας στον κώδικα.

Για το λόγο αυτό, το έργο αυτό ασχολείται μόνο με "απλές" κλίσεις. Εκείνοι που ξεκινούν με ένα χρώμα στο ένα άκρο της όψης και ξεθωριάζουν σε άλλο χρώμα στην αντίθετη άκρη.

Εάν πρέπει να προσθέσετε άλλη στάση, θα πρέπει να είστε σε θέση να τροποποιήσετε τον κώδικα αρκετά εύκολα, αλλά θα καθιστούσε υπερβολικά περίπλοκο το σεμινάριο.

Έτσι, η εφαρμογή των στάσεων χρωμάτων είναι απλά:

// αρχίζει το χρώμα
@IBΙναθετό var startColor: UICoror;
// το χρώμα στο τέλος της κλίσης
@IBIνα διαπιστωθεί var endColor: UICoror;

Αυτά εμφανίζονται στο Interface Builder ως ωραία χρώματα ελέγχου.

Καθορισμός της κατεύθυνσης

Η κατεύθυνση της κλίσης ορίζεται από δύο ιδιότητες στο CAGradientLayer:

// Το τελικό σημείο της κλίσης όταν σχεδιάζεται στο χώρο συντεταγμένων του στρώματος. Ζωντανό.
var endPoint: CGPoint
// Το σημείο εκκίνησης της κλίσης όταν σχεδιάζεται στο χώρο συντεταγμένων του στρώματος. Ζωντανό.
var startPoint: CGPoint

Τα αρχικά και τα τελικά σημεία της διαβάθμισης καθορίζονται στον χώρο κλίσης μονάδων, πράγμα που απλά σημαίνει ότι ανεξάρτητα από τις διαστάσεις ενός δεδομένου CAGradientLayer, στον χώρο της διαβάθμισης μονάδων θεωρούμε ότι το επάνω αριστερό είναι η θέση (0, 0) και το κάτω δεξιά για να είναι θέση (1, 1), όπως απεικονίζεται παρακάτω:

Αυτό είναι το σύστημα συντεταγμένων CAGradientLayer.

Η κατεύθυνση είναι το πιο δύσκολο μέρος της δημιουργίας κλίσης @IBDesignable. Λόγω της ανάγκης για ένα σημείο έναρξης και τέλους, το γεγονός ότι τα χαρακτηριστικά @ IBInspectable δεν υποστηρίζουν τον τύπο δεδομένων CGPoint, για να μην αναφέρουμε την πλήρη έλλειψη επικύρωσης δεδομένων στο UI, οι επιλογές μας είναι λίγο περιορισμένες.

Όταν προσπαθούμε να επεξεργαστούμε τον απλούστερο τρόπο καθορισμού κοινών οδηγιών διαβάθμισης, μια συμβολοσειρά φαινόταν ένας δυνητικά χρήσιμος τύπος δεδομένων και ίσως σημεία πυξίδας, π.χ. "N", "S", "E", "W" μπορεί να είναι χρήσιμα. Αλλά για τις ενδιάμεσες κατευθύνσεις θα πρέπει να υποστηρίζει "NW"; Τι γίνεται με το "NNW" ή το "WNW"; Και πέρα ​​από αυτό; Αυτό θα είχε άμεση σύγχυση. Και αυτός ο τρόπος σκέψης ήταν σαφώς μακρύς δρόμος για να συνειδητοποιήσουμε ότι ο καλύτερος τρόπος να περιγράψουμε οποιαδήποτε γωνία στην πυξίδα χρησιμοποιούσε βαθμούς!

Ο χρήστης μπορεί να ξεχάσει τους χώρους κλίσης μονάδων. Όλη η πολυπλοκότητα μειώνεται σε μία μόνο ιδιότητα που εκτίθεται στο Interface Builder:

// η γωνία κλίσης, σε μοίρες αριστερόστροφα από 0 (ανατολικά / δεξιά)
@IBIναθετήσιμη γωνία var: CGFloat = 270

Η προεπιλεγμένη τιμή (270 μοίρες) σημειώνει νότια απλά για να ταιριάζει με την προεπιλεγμένη κατεύθυνση CAGradientLayer. Για οριζόντια κλίση, ρυθμίστε το σε 0 ή 180.

Μετατροπή της γωνίας σε χώρο κλίσης

Αυτό είναι το πιο δύσκολο κομμάτι. Περιλαμβάνω τον κώδικα και μια περιγραφή του τρόπου με τον οποίο λειτουργεί. Μπορείτε να το παρακάμψετε αν ενδιαφέρεστε απλώς να χρησιμοποιήσετε την τάξη.

Η λειτουργία κορυφαίου επιπέδου για τη μετατροπή της γωνίας στο σημείο εκκίνησης και στο τέλος των σημείων κλίσης εμφανίζεται ως εξής:

// Δημιουργία διανύσματος που δείχνει προς την κατεύθυνση της γωνίας
ιδιωτικά λειτουργικά σημεία βαθμών (_ γωνία: CGFloat) -> (CGPoint, CGPoint) {
  // πάρτε αρχικά και τελικά σημεία διανύσματος
  ας τέλος = pointForAngle (γωνία)
  ας ξεκινήσει = opposePoint (τέλος)
  // μετατρέψτε σε χώρο κλίσης
  ας p0 = transformToGradientSpace (αρχή)
  ας p1 = transformToGradientSpace (τέλος)
  επιστροφή (p0, p1)
}}

Αυτό παίρνει τη γωνία που καθορίζει ο χρήστης και τον χρησιμοποιεί για να δημιουργήσει ένα διάνυσμα που δείχνει προς αυτή την κατεύθυνση, όπως απεικονίζεται παρακάτω. Η γωνία καθορίζει την περιστροφή του διανύσματος από 0 μοίρες. Με συμβατικά σημεία ανατολικά στην Core Animation, και αυξάνεται αριστερόστροφα (αριστερόστροφα).

Το τελικό σημείο βρίσκεται από το call pointForAngle (), που ορίζεται ως εξής:

ιδιωτικό λειτουργικό σημείο λειτουργίας (_ γωνία: CGFloat) -> CGPoint {
  // μετατρέπουν τους βαθμούς σε ακτίνια
  ας radians = γωνία * .pi / 180.0
  var x = cos (ακτίνια)
  var y = sin (ακτίνια)
  // (x, y) είναι σε όρους κύκλου μονάδων. Επεξάγετε την τετραγωνική μονάδα για να έχετε πλήρες μήκος φορέα
  εάν (fabs (x)> fabs (y)) {
    // extraplate x στο μήκος μονάδας
    x = χ> 0; 1: -1 y = x * tan (ακτίνια)
  } else {
    // extrapolate στο μήκος μονάδας
    y = y> 0; 1: -1
    x = y / tan (ακτίνια)
  }}
  επιστροφή CGPoint (x: x, y: y)
}}

Αυτή η λειτουργία φαίνεται πιο περίπλοκη από ό, τι είναι. Στον πυρήνα του, παίρνει απλά το ημίτονο και το συνημίτονο της γωνίας για να καθορίσει το τελικό σημείο σε ένα κύκλο μονάδας. Επειδή η τριγωνομετρία του Swift (κοινά με τις περισσότερες άλλες γλώσσες) απαιτεί γωνίες που πρέπει να καθορίζονται σε ακτίνια και όχι σε μοίρες, πρέπει πρώτα να κάνουμε αυτή τη μετατροπή. Στη συνέχεια, η τιμή x υπολογίζεται από το x = cos (radians), και από την τιμή y από y = sin (radians).

Η υπόλοιπη λειτουργία ασχολείται με το γεγονός ότι το προκύπτον σημείο βρίσκεται στον κύκλο μονάδας. Τα σημεία που χρειαζόμαστε, ωστόσο, βρίσκονται σε ένα τετράγωνο μονάδων. Οι γωνίες κατά μήκος των σημείων πυξίδας (δηλ. 0, 90, 180 και 270 μοίρες) θα δώσουν το σωστό αποτέλεσμα, στην άκρη του τετραγώνου. Για ενδιάμεσες γωνίες, το σημείο θα τοποθετηθεί από την άκρη του τετραγώνου. Ο φορέας πρέπει να παρεκταθεί στην άκρη του τετραγώνου για να δώσει τη σωστή οπτική εμφάνιση. Αυτό απεικονίζεται παρακάτω.

Τώρα έχουμε το τελικό σημείο σε ένα τετράγωνο με υπογεγραμμένη μονάδα, το σημείο εκκίνησης του διανύσματος βρίσκεται με την απλή λειτουργία παρακάτω. Επειδή το σημείο βρίσκεται σε ένα υπογεγραμμένο χώρο μονάδων, είναι ασήμαντο να βρείτε το σημείο εκκίνησης απλά αναστρέφοντας το σημείο των στοιχείων του τελικού σημείου.

ιδιωτικό λειτουργικό opposePoint (_ point: CGPoint) -> CGPoint {
  επιστροφή CGPoint (x: -point.x, y: -point.y)
}}

Σημειώστε ότι ένας άλλος τρόπος για να επιτευχθεί αυτό θα ήταν να προσθέσετε 180 μοίρες στην αρχική γωνία και callpointForAngle () πάλι. Αλλά η μέθοδος ανατροπής σημείου είναι τόσο απλή που είναι ελαφρώς πιο αποτελεσματική για να το κάνετε αυτό.

Τώρα έχουμε τα αρχικά και τα τελικά σημεία στον υπογεγραμμένο χώρο μονάδων, το μόνο που απομένει είναι να μεταφράσουμε τους στο μη υπογεγραμμένο χώρο κλίσης. Ο χώρος μονάδας έχει έναν άξονα y που αυξάνεται προς τα βόρεια. Ενώ στον κεντρικό animation χώρο y αυξάνεται προς τα νότια. Επομένως, το στοιχείο y πρέπει να αναστραφεί ως μέρος αυτής της μετάφρασης. Η θέση (0, 0) στο υπογεγραμμένο χώρο μονάδων γίνεται (0,5, 0,5) στο χώρο κλίσης. Η λειτουργία είναι πολύ απλή:

ιδιωτικό func transformToGradientSpace (_ σημείο: CGPoint) -> CGPoint {
  // το σημείο εισόδου βρίσκεται στον υπογεγραμμένο χώρο μονάδων: (-1, -1) έως (1,1)
  // μετατρέπεται σε χώρο μεταβαλλόμενης κλίσης: (0,0) έως (1,1), με ανασηκωμένο άξονα Υ
  επιστροφή CGPoint (x: (σημείο.x + 1) * 0.5, y: 1.0 - (σημείο.y + 1) * 0.5)
}}

Συγχαρητήρια!

Και αυτό είναι όλη η σκληρή δουλειά - φουά! Συγχαρητήρια για να φτάσετε αυτό μακριά - πηγαίνετε να πάρετε έναν καφέ για να γιορτάσετε ...

Υποστήριξη οικοδόμων διασύνδεσης

Όλα τα υπόλοιπα της κλάσης προβολής κλίσης είναι η συνάρτηση prepareForInterfaceBuilder (). Αυτή η λειτουργία εκτελείται μόνο από το με το Interface Builder όταν χρειάζεται να προβάλει μια προβολή. Μια σωστά σχεδιασμένη προβολή @IBDesignable μπορεί πραγματικά να λειτουργήσει αρκετά καλά χωρίς αυτήν. Θα υπάρξουν χρόνοι - για παράδειγμα όταν προσθέτετε μια νέα προβολή σε ένα storyboard - όταν δεν θα εμφανιστεί σωστά μέχρι να εμφανιστεί αυτή η λειτουργία. Μπορείτε να τον αναγκάσετε να εκτελεστεί επιλέγοντας την προβολή στον πίνακα πλοήγησης και επιλέγοντας τον επεξεργαστή Editor | Debug Selected Views από το μενού.

Η εφαρμογή της συνάρτησης απλά διασφαλίζει ότι η κλίση είναι εγκατεστημένη και ενημερωμένη.

override func prepareForInterfaceBuilder () {
  super.prepareForInterfaceBuilder ()
  installGradient ()
  updateGradient ()
}}

Ευχαριστώ για την ανάγνωση!

Ο κώδικας για αυτό το έργο είναι ελεύθερα διαθέσιμος στο GitHub.

Ο Lee Dowthwaite είναι ένας βετεράνος προγραμματιστής iOS, ο οποίος έχει αναπτύξει εφαρμογές υψηλού προφίλ για πολλούς κορυφαίους πελάτες από το 2010.

Αρχικά δημοσιεύθηκε στο appcodelabs.com στις 10 Δεκεμβρίου 2017.