État et liaison du flux de données SwiftUI

Communauté de combat Python

Communauté de combat Java

Appuyez longuement pour identifier le code QR ci-dessous, ajoutez au besoin

Scannez le code QR à suivre pour ajouter un service client

Entrez dans la communauté Python ▲

Scannez le code QR à suivre pour ajouter un service client

Entrez dans la communauté Java

Auteur 丨 Équipe technique Huyou

Source 丨 Produits technologiques Sohu (ID: sohu-tech)

Dans SwiftUI, un mécanisme de mise à jour de l'état basé sur les données est construit avec une source unique de vérité comme noyau. Une variété de nouveaux wrappers de propriété ont été introduits pour la gestion des états. Cet article présente principalement @State et @Binding, en commençant par une utilisation simple, en montrant leurs scénarios d'utilisation à travers une série d'exemples de code spécifiques et en progressant pour explorer les principes d'implémentation interne de State.

alentours

  • MacOS 10.15.5

  • Xcode 12.0 bêta

@Etat

Un type de wrapper de propriété qui peut lire et écrire une valeur gérée par SwiftUI.

@State est un wrapper de propriété (propriété wrapper), conçu pour la gestion de l'état des types valeur; utilisé pour les types valeur mutables dans Struct

struct User {
    var firstName = "Bilbo"
    var lastName = "Baggins"
}

struct ContentView: View {
    @State private var user = User()  //1

    var body: some View {
        VStack {
            Text("Your name is \(user.firstName) \(user.lastName).")  //2
            TextField("First name", text: $user.firstName) //3
            TextField("Last name", text: $user.lastName)
        }
    }
}
  • L'accès aux attributs modifiés par @State ne peut avoir lieu que dans le corps ou les méthodes appelées par body. Vous ne pouvez pas modifier la valeur de @State en externe. Vous ne pouvez définir la valeur initiale que lorsque @State est initialisé, comme indiqué dans la note 1. Toutes les opérations et changements d'état associés doivent être cohérents avec le cycle de vie actuel de View.

  • Dans l'attribut du package de référence en tant que @State, s'il est à la fois en lecture et en écriture, l'attribut de référence doit commencer par $ (note 3), s'il est en lecture seule, utilisez directement le nom de la variable (note 2)

  • L'état gère les variables internes de la vue spécifique et ne doit pas être autorisé à y accéder de l'extérieur, il doit donc être marqué comme privé (note 1)

Cependant, si vous struct Userremplacez par class Usersera invalide, pourquoi?

@State détecte le type de valeur

  • Le type valeur n'a qu'un propriétaire indépendant et le type de classe peut pointer vers un; pour deux vues SwiftUI, même si deux objets struct identiques leur sont envoyés, en fait, ils reçoivent chacun une copie distincte de la vue Copie de struct, donc la valeur de struct de l'une des vues change, ce qui n'a aucun effet sur l'autre; au contraire, si c'est une classe, cela va s'influencer mutuellement;

  • Lorsque User est une structure, chaque fois que nous modifions les propriétés de cette structure, Swift crée en fait une nouvelle instance de structure. @State peut détecter ce changement et recharger automatiquement notre vue. Maintenant, si nous changeons de classe, nous avons une classe, ce comportement ne se produira plus, Swift peut directement modifier la valeur.

Rappelez-vous comment nous avons utilisé le mot-clé mutating pour modifier les propriétés des méthodes structurelles?

struct User {
 var name:String
 mutating func changeName(name:String) {
      self.name = name
  }
}

En effet, si nous créons une propriété de structure en tant que variable, mais que la structure elle-même est une constante, nous ne pouvons pas modifier la propriété; lorsque la propriété change, Swift doit être en mesure de détruire et de recréer toute la structure, ce qui est pour les structures constantes. impossible. La classe n'a pas besoin du mot-clé mutating, car même si l'instance de classe est marquée comme constante, Swift peut toujours modifier les propriétés de la variable.

Si User est une classe, les propriétés elles-mêmes ne changeront pas, donc @State ne remarquera rien et ne pourra pas recharger la vue. Même si la valeur d'une certaine propriété de la classe change, @State ne les surveille pas, la vue ne sera donc pas rechargée.

Si vous souhaitez modifier cette situation afin que la classe soit surveillée pour les modifications, vous ne pouvez pas utiliser @State, vous devez utiliser @ObservedObject ou @StateObject

@Contraignant

Un type de wrapper de propriété qui peut lire et écrire une valeur appartenant à une source de vérité.

Le rôle de @Binding est de créer une connexion bidirectionnelle entre les attributs qui sauvegardent l'état et la vue qui modifie les données, en connectant les attributs actuels à une seule source de vérité stockée ailleurs, plutôt que de stocker directement les données. Pour convertir l'attribut sémantique de la valeur stockée ailleurs dans la sémantique de référence, vous devez ajouter le signe $ au nom de la variable lors de son utilisation.

Le scénario d'utilisation habituel consiste à transmettre le type de valeur @State de la vue actuelle à sa vue enfant. Si le type de valeur @State est transmis directement, le type de valeur sera copié. Ensuite, si une propriété du type valeur est exécutée dans la vue enfant. Modification, la vue parent ne sera pas modifiée, vous devez donc transférer @State vers @Binding.

Les attributs modifiés @Binding n'ont pas besoin d'avoir des valeurs initialisées. La liaison peut être utilisée avec les attributs de valeur dans les objets @State ou ObservableObject. Notez qu'il ne s'agit pas d'un wrapper d'attribut @ObservedObject

struct Product:Identifiable {
    var isFavorited:Bool
    var title:String
    var id: String
}

struct FilterView: View {
    @Binding var showFavorited: Bool  //3

    var body: some View {
        Toggle(isOn: $showFavorited) {  //4
            Text("Change filter")
        }
    }
}

struct ProductsView: View {
    let products: [Product] = [
    Product(isFavorited: true, title: "ggggg",id: "1"),
    Product(isFavorited: false, title: "3333",id: "2")]

    @State private var showFavorited: Bool = false   //1

    var body: some View {
        List {
            FilterView(showFavorited: $showFavorited)  //2

            ForEach(products) { product in
                if !self.showFavorited || product.isFavorited {
                    Text(product.title)
                }
            }
        }
    }
}

Cet exemple montre une liste avec un commutateur de filtre. Afin de simplifier le contenu pour expliquer le problème principal, il n'y a que deux lignes de contenu. La vue parente est ProductsView et la vue enfant FilterView et les éléments de liste sont imbriqués afin de permettre le transfert de la modification de showFavorited dans FilterView. Retour à la vue parent:

  • Note 1, showFavorited est décoré avec @State

  • Note 2: Dans le corps, récupérez la liaison correspondant à showFavorited via $ showFavorited et transmettez-la à la sous-vue FilterView

  • Remarque 3: @Binding var showFavorited: BoolLes paramètres entrants de référence sont définis dans la sous-vue FilterView

  • Remarque 4, lorsque le commutateur est commuté, la source de vérité unique externe (source unique de vérité) sera modifiée en raison du mécanisme @Binding, de sorte que le contenu affiché dans la liste sera filtré en continu selon les conditions

Mutable et immuable

Tout d'abord, explorons un problème à l'aide de l'exemple suivant

struct StateMutableView: View {
    @State private var flag = false
    private var anotherFlag = false

    mutating func changeAnotherFlag(_ value: Bool) {
        self.anotherFlag = value
    }
    
    var body: some View {
        Button(action: {
            //1 ok
            self.flag = true
            
            //2 Cannot assign to property: 'self' is immutable
            self.anotherFlag = true
            
            //3 Cannot use mutating member on immutable value: 'self' is immutable
            changeAnotherFlag(true)
        }) {
            Text("Test")
        }
    }
}

Flag est une variable marquée comme State, anotherFlag est une variable ordinaire qui n'utilise pas de wrappers d'attributs, et une méthode de mutation changeAnotherFlagest ajoutée pour modifier un anotherFlag;

Modifiez les deux variables du corps de plusieurs manières. Les notes 1 à 3 marquent respectivement les résultats de la modification et les erreurs d'invite. De toute évidence, l'indicateur peut être modifié, mais pas un autreFlag. Pourquoi?

Deux problèmes sont impliqués ici:

  1. Pourquoi le drapeau peut-il être modifié?

  2. Pourquoi un autreFlag ne peut-il pas être modifié?

Regardons d'abord la deuxième question

Pourquoi un autreFlag ne peut-il pas être modifié

Getter de propriété calculée

Exemple 5

struct SimpleStruct {
    var anotherFlag: Bool {
        _anotherFlag = true
//      ^~~~~~~~~~~~
//      error: cannot assign to property: 'self' is immutable
        return _anotherFlag
    }

    private var _anotherFlag = false
}

_anotherFlag stocke les attributs, et l'attribut calculé d'un autreFlag se trouve dans l'attribut getter. Self est sans mutation par défaut et ne peut pas être modifié, une erreur est donc signalée

Cependant, il peut y avoir des exceptions. Si le getter est spécialement marqué comme mutant, il peut être modifié

struct SimpleStruct {
    var anotherFlag: Bool {
        mutating get {
            _anotherFlag = true
            return _anotherFlag
        }
    }

    private var _anotherFlag = false
}

Et aussi besoin d'utiliser SimpleStruct, déclarez l'instance comme var

var s0 = SimpleStruct()
_ = s0.anotherFlag // ok, and modifies s0

let s1 = SimpleStruct()
_ = s1.anotherFlag
//  ^~ error: cannot use mutating getter on immutable value: 's1' is a 'let' constant

Puisque self peut être modifié dans la propriété calculée get en ajoutant la mutation, la propriété body de l'exemple précédent dans SwiftUI peut-elle être ajoutée?

Afficher la définition du protocole View

public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}

Le corps est défini et ne peut pas être changé en mutation, donc si vous le modifiez comme suit

struct SimpleView: View {
//     ^ error: type 'SimpleView' does not conform to protocol 'View' 
    var body: some View {
        mutating get { Text("Hello") }
    }
}

Une erreur sera signalée, indiquant que le protocole View n'est pas suivi

Résumé: La raison pour laquelle un autreFlag ne peut pas être modifié: le getter de la propriété calculée du corps ne peut pas être modifié par mutation

Pourquoi le drapeau peut-il être modifié

Puisque SwiftUI a été conçu au début, on espère que l'arborescence de vues construite restera inchangée, de sorte que l'interface utilisateur puisse être rendue efficacement et que les modifications puissent être suivies. Lorsque la variable marquée comme @State change, la variable elle-même ne peut pas être modifiée dans Struct, donc l'état est Le wrapper de propriété de l'exemple consiste essentiellement à modifier des variables en dehors de la structure actuelle

Regardons la définition de l'État

@frozen @propertyWrapper public struct State<Value> : DynamicProperty {

    /// Initialize with the provided initial value.
    public init(wrappedValue value: Value)

    /// Initialize with the provided initial value.
    public init(initialValue value: Value)

    /// The current state value.
    public var wrappedValue: Value { get nonmutating set }

    /// Produces the binding referencing this state value
    public var projectedValue: Binding<Value> { get }
}

WrappedValue est marqué comme un ensemble non mutant et utilise directement la valeur wrappedValue utilisée par l'objet state et la valeur projetée utilisée par le symbole $

Quelle est la signification de non mutation?

Définisseur de propriétés calculé

Dans l'attribut setter, self est en mutation par défaut et peut être modifié; nous ne pouvons pas attribuer une valeur à une quantité immuable, nous pouvons rendre l'attribut assignable en déclarant setter nonmutating. Ce mot clé nonmutating indique au compilateur que le processus d'affectation ne modifiera pas cela struct lui-même, mais modifiez d'autres variables.

struct SimpleStruct {
    var anotherFlag: Bool {
        mutating get {
            _anotherFlag = true
            return _anotherFlag
        }
    }

    private var _anotherFlag: Bool {
        get {
            return UserDefaults.standard.bool(forKey: "storage")
        }
        nonmutating set {
            UserDefaults.standard.setValue(newValue, forKey: "storage")
        }
    }
}

let s0 = SimpleStruct()
var s1 = s0
_ = s1.anotherFlag // 同时影响s0和s1,他们内部的_anotherFlag都发生了变化

Dans cet exemple, _anotherFlag modifie la valeur de UserDefaults, ce qui affectera à la fois s0 et s1, ce qui équivaut à jouer le rôle de type référence. En programmation réelle, c'est bien sûr un mauvais exemple, qui est sujet à des problèmes.

Résumé: La raison pour laquelle l'indicateur peut être modifié est que l'attribut de wrapper de propriété est ajouté. La variable elle-même n'a pas changé, mais la variable en dehors de la structure actuelle gérée par SwiftUI est modifiée

Implémentation interne @State

Pour une analyse plus approfondie, je


  • Afin d'analyser l'état des variables, à la ligne 16, la méthode init de la structure User; ligne 39, la fin de la méthode init de ContentView; ligne 47, le bouton est cliqué pour exécuter la partie fonction, et des points d'arrêt sont ajoutés.

  • Puisque @State est pour les types valeur, afin d'imprimer l'adresse de la structure, la fonction d'adresse est ajoutée

  • fonction système de vidage, capable d'imprimer la structure interne des variables

L'interface en cours d'exécution est illustrée dans la figure ci-dessus. La zone de saisie de cet article peut modifier le nom et le bouton Nombre + 1 augmente le nombre de comptage de 1.

Ouvrez le point d'arrêt, exécutez le code depuis le début, exécutez d'abord jusqu'au point d'arrêt de la 16e ligne, l'utilisateur est initialisé, à ce stade, self est la structure utilisateur elle-même

▿ User
 \- name : ""
 \- count : 0

Continuez l'exécution jusqu'à la dernière ligne de la méthode d'initialisation ContentView, à ce moment, self est ContentView, imprimez-le

▿ ContentView
 ▿ _user : State<User>
  ▿ _value : User
   \- name : ""
   \- count : 0
  \- _location : nil

Une nouvelle _uservariable apparaît , le type est State<User>, le _valuetype d' attribut interne de cette variable est User; cela signifie que la variable d'instance utilisateur avec le wrapper d'attribut @State passe de son propre Usertype à un nouveau State<User>type, le nouveau type étant complété par cette transformation L'instance _userest générée et gérée par SwiftUI, et son package interne est la véritable instance User.De plus _location, il convient de noter qu'elle est actuellement nulle;

Si vous remarquez 35 lignes de code et user = User(name: "TT", count: 100)constatez que cela ne change pas les internes _user, si vous souhaitez modifier, vous ne pouvez utiliser que la méthode suivante, via la deuxième méthode d'initialisation fournie par State

_user = State(wrappedValue: User(name: "TT", count: 100))

En même temps, vérifiez la sortie du journal de la console actuelle

User init
ContentView init
140732783334216
▿ SwiftUI.State<DemoState.User>
  ▿ _value: DemoState.User
    - name: ""
    - count: 0
  - _location: nil

Selon la séquence d'exécution attendue, User init est exécuté, ContentView init est exécuté, puis l'adresse et _userla structure interne de la structure actuelle sont imprimées

Ensuite, puisque le corps est exécuté et que la page est rendue complètement, cliquez maintenant sur le bouton Count + 1 et le point d'arrêt s'arrête à la ligne 47

▿ ContentView
  ▿ _user : State<User>
    ▿ _value : User
      - name : ""
      - count : 0
    ▿ _location : Optional<AnyLocation<User>>
      ▿ some : <StoredLocation<User>: 0x600003c26a80>

_locationPlus nul

140732783330824
▿ SwiftUI.State<DemoState.User>
  ▿ _value: DemoState.User
    - name: ""
    - count: 0
  ▿ _location: Optional(SwiftUI.StoredLocation<DemoState.User>)
    ▿ some: SwiftUI.StoredLocation<DemoState.User> #0

Notez que l'adresse de l'utilisateur a changé. L'utilisateur créé au début est détruit et recréé. En effet, toutes les opérations associées et les changements d'état des attributs modifiés par @State doivent être cohérents avec le cycle de vie de la vue actuelle. Lorsque la vue Lorsque l'initialisation n'est pas terminée, la relation de liaison entre l'attribut state et la vue ne peut pas être complétée; elle n'est _locationpas nulle, ce qui économise beaucoup d'informations marquant l'unicité de la vue, qui n'est pas toutes montrées ici;

Cliquez à nouveau sur le bouton Count + 1, la valeur de comptage devient 2, l'adresse de l'utilisateur continuera à rester inchangée et le cycle de vie sera cohérent avec la vue.

Grâce à l'analyse précédente, _userl'existence de variables internes a été clarifiée , l'analyse complémentaire suivante de la relation entre wrappedValue et projetéeValue dans la mise en œuvre interne de l'État

(lldb) p _user
(State<DemoState.User>) $R6 = {
  _value = (name = "", count = 2)
  _location = 0x0000600003c26a80 {
    SwiftUI.AnyLocationBase = {}
  }
}

(lldb) p _user.wrappedValue
(DemoState.User) $R8 = (name = "", count = 2)

(lldb) p _user.projectedValue
(Binding<DemoState.User>) $R10 = {
  transaction = {
    plist = {
      elements = nil
    }
  }
  location = 0x0000600003c26a80 {
    SwiftUI.AnyLocationBase = {}
  }
  _value = (name = "", count = 2)
}

SwiftUI se @State var user = User()convertit en trois attributs

private var _user: State<User> = State(initialValue: User())
private var $user: Binding<User> { return _user.projectedValue }
private var user: User {
    get { return _user.wrappedValue }
    nonmutating set { _user.wrappedValue = newValue }
}

Pourquoi $ user est-il en lecture seule? Testez-le constatera que la modification échoue

(lldb) expr $user = User(name:"",count:100)
error: <EXPR>:3:1: error: cannot assign to property: '$user' is immutable
$user = User(name:"",count:100)
^~~~~

error: <EXPR>:3:9: error: cannot assign value of type 'User' to type 'Binding<User>'
$user = User(name:"",count:100)
        ^~~~~~~~~~~~~~~~~~~~~~~
        
(lldb) expr $user.name = "Tim"
error: <EXPR>:3:7: error: cannot assign to property: '$user' is immutable
$user.name = "Tim"
~~~~~ ^

error: <EXPR>:3:14: error: cannot assign value of type 'String' to type 'Binding<String>'
$user.name = "Tim"
             ^~~~~ 

Description Propriété en lecture seule projectValue

Grâce à l'analyse ci-dessus, vous pouvez dessiner une image de la relation entre les propriétés de l'État.

_user:State<User>
 _value:User
  _name:String
  _count:Int
 _wrappedValue:User 
  get { _value }
  set { _value = newValue }
 _projectedValue:User 
  get { _value }

Nous pouvons en outre écrire grossièrement une logique d'implémentation possible de State

@propertyWrapper struct State<T> {
    var _value:T
    
    init(wrappedValue: T) {
        _value = wrappedValue
    }

    var wrappedValue: T {
        nonmutating set { _value = newValue }   
        get { _value.value }
    }

    var projectedValue: T { _value }
}

Pour résumer

  • L'encapsuleur d'attributs @State effectue la gestion de l'état des types de valeur, utilisé pour les types de valeur mutables dans Struct, et toutes les opérations et changements d'état associés sont cohérents avec le cycle de vie actuel de View

  • La liaison convertit les propriétés sémantiques de la valeur stockée ailleurs en sémantique de référence, et doit ajouter un signe $ au nom de la variable lors de son utilisation

  • Ajout des propriétés du wrapper de propriété, la variable elle-même n'a pas changé, mais a modifié les variables autres que la structure actuelle maintenue par SwiftUI

référence

  • https://developer.apple.com/documentation/swiftui/state

  • https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject

  • https://kateinoigakukun.hatenablog.com/entry/2019/03/22/184356

<LA FIN>

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

Contenu passionnant récent recommandé:  

 Comparaison des revenus des programmeurs en Chine, aux États-Unis, au Japon et en Inde

 Un triste jour pour les programmeurs

 SringMVC de l'entrée au code source, celui-ci suffit

 10 animations visuelles Python, soigneusement et magnifiquement


Regardez le bon article ici pour le partager avec plus de gens ↓↓

Je suppose que tu aimes

Origine blog.csdn.net/Px01Ih8/article/details/109281383
conseillé
Classement