SwiftUI en los estantes

Cada vez que aparece un nuevo marco en un lenguaje de programación, tarde o temprano, aparecen personas que aprenden el lenguaje de él. Este fue probablemente el caso en el desarrollo de iOS en el momento de la aparición de Swift: al principio se consideró como una adición a Objective-C, pero todavía no lo encontré. Ahora, si comienza desde cero, la selección del idioma ya no vale la pena. Swift está más allá de la competencia.

Lo mismo, pero a menor escala, está sucediendo con los marcos. La aparición de SwiftUI no es una excepción. Probablemente soy un representante de la primera generación de desarrolladores que comenzaron aprendiendo SwiftUI, ignorando UIKit. Esto tiene un precio: hasta ahora hay muy pocos materiales de capacitación y ejemplos de código de trabajo. Sí, la red ya tiene varios artículos que hablan sobre una característica particular, una herramienta particular. En el mismo www.hackingwithswift.com ya hay muchos ejemplos de código con explicaciones. Sin embargo, hacen poco para ayudar a aquellos que deciden aprender SwiftUI desde cero, como yo. La mayoría de los materiales en la red son respuestas a preguntas específicas formuladas. Un desarrollador experimentado puede descubrir fácilmente cómo funciona todo, por qué es así y por qué debería aplicarse. Para un principiante, primero debe comprender qué pregunta hacer, y solo entonces puede llegar a estos artículos.



Debajo del corte, intentaré sistematizar y clasificar lo que he logrado aprender en este momento. El formato del artículo es casi una guía, aunque más bien, una hoja de trucos compilada por mí en la forma en que me gustaría leerla al comienzo de mi viaje. Para los desarrolladores experimentados que aún no han profundizado en SwiftUI, también hay un par de ejemplos de código interesantes, y las explicaciones textuales se pueden leer en diagonal.

Espero que este artículo te ahorre algo de tiempo cuando también quieres sentir un poco de magia.

Para empezar, un poco sobre ti


Prácticamente no tengo experiencia en desarrollo móvil y experiencia significativa
en 1s no podría ayudar mucho aquí. Sobre cómo y por qué decidí aprender SwiftUI, te diré en otro momento, si será interesante para alguien, por supuesto.

Sucedió que el comienzo de mi inmersión en el desarrollo móvil coincidió con el lanzamiento de iOS 13 y SwiftUI. Esta es una señal, pensé, y decidí comenzar de inmediato con ella, ignorando UIKit. Me pareció una coincidencia divertida que comencé a trabajar con 1c en esos momentos: luego aparecieron los formularios administrados. En el caso de 1c, la popularización de la nueva tecnología tomó casi cinco años. Cada vez que se instruía a un desarrollador para que implementara alguna funcionalidad nueva, se enfrentaba a una elección: hacerlo de manera rápida y confiable, con herramientas conocidas, o pasar mucho tiempo buscando nuevas y sin garantías de resultados. La elección generalmente se hizo a favor de la velocidad y la calidad en este momento, y la inversión de tiempo en nuevas herramientas se retrasó por mucho tiempo.

Ahora, aparentemente, la situación con SwiftUI es casi la misma. Todos están interesados, todos entienden que este es el futuro, pero hasta ahora pocos se han tomado el tiempo para estudiarlo. A menos que sea para proyectos de mascotas.

En general, no me importaba qué marco estudiar, y decidí arriesgarme a pesar de la opinión general de que sería posible lanzarlo en producción en uno o dos años. Y dado que resultó ser uno de los pioneros, decidí compartir mi experiencia práctica. Quiero decir que no soy un gurú, y en general en el desarrollo móvil, un hervidor de agua. Sin embargo, ya he recorrido un cierto camino, durante el cual busqué en Internet en busca de información, y puedo decir con seguridad que no es suficiente y que prácticamente no está sistematizado. Pero en ruso, por supuesto, es prácticamente inexistente. Si es así, decidí reunir fuerzas, alejar el complejo de impostores y compartir con la comunidad lo que logré resolver por mí mismo. Procederé de la suposición de que el lector ya está al menos mínimamente familiarizado con SwiftUI, y no descifraré cosas como VStack{…} , Text(…) , etc.

Una vez más, enfatizo que describiré mejor mis propias impresiones de los intentos de lograr el resultado deseado de SwiftUI. Bien podría no entender algo, y de algunos experimentos para sacar conclusiones erróneas o inexactas, por lo que cualquier corrección y aclaración son bienvenidas.

Para los desarrolladores experimentados, este artículo puede parecer lleno de descripciones de cosas obvias, pero no juzgue estrictamente. Todavía no se han escrito tutoriales para dummies en SwiftUI.

¿Qué demonios es este SwiftUI?


Entonces, probablemente comenzaría con lo que se trata, esta es su SwiftUI. Aquí nuevamente, surge mi primer pasado. La analogía con los formularios administrados solo se fortaleció cuando vi algunos videos tutoriales sobre cómo diseñar interfaces en Storyboard (es decir, cuando trabajaba con UIKit). Tomé nostalgia de acuerdo con formas "incontrolables" en 1s: colocación manual de elementos en el formulario, y especialmente enlaces ... Oh, cuando el autor del video de capacitación durante unos 20 minutos habló sobre las complejidades de la unión de varios elementos entre sí y los bordes de la pantalla, recordé con una sonrisa 1C - todo era igual antes de las formas controladas. Bueno, casi ... un poco peor, por supuesto, bueno, y en consecuencia, más fácil. Y SwiftUI es, más o menos, formularios gestionados de Apple. Sin ataduras. No hay guiones gráficos y segways. Simplemente describe la estructura de su Vista en código. Y eso es todo. Todos los parámetros, tamaños, etc. se configuran directamente en el código, pero de manera bastante simple. Más precisamente, puede editar los parámetros de los objetos existentes en Canvas, pero para esto, primero debe agregarlos en el código. Honestamente, no sé cómo funcionará esto en grandes equipos de desarrollo, donde se acostumbra separar el diseño del diseño y el contenido de la Vista en sí, pero como desarrollador independiente realmente me gusta este enfoque.

Estilo declarativo


SwiftUI asume que la descripción de la estructura de su Vista está completamente en código. Además, Apple nos ofrece un estilo declarativo de escribir este código. Es decir, algo como esto:
“Esta es una vista. (Por alguna razón, quiero decir "ver" y, en consecuencia, aplicar declinación a una palabra femenina) consta de dos campos de texto y una imagen. Los campos de texto están dispuestos uno tras otro horizontalmente. La imagen está debajo de ellos y sus bordes se recortan en forma de círculo ".
Suena inusual, ¿verdad? Por lo general, en el código describimos el proceso en sí, lo que hay que hacer para lograr el resultado que tenemos en nuestra cabeza:
"Inserte un bloque, inserte un campo de texto en este bloque, seguido de otro campo de texto, y después de eso, tome una foto, recorte sus bordes redondeándolos y péguelos debajo".
Suena como una instrucción para muebles de Ikea. Y en swiftUI, vemos de inmediato cuál debería ser el resultado. Incluso sin Canvas o depuración, la estructura del código refleja claramente la estructura de la Vista. Está claro qué y en qué secuencia se mostrará y con qué efectos.

Un excelente artículo sobre FunctionBuilder y cómo le permite escribir código en un estilo declarativo ya está en Habré .

En principio, se ha escrito mucho sobre el estilo declarativo y sus ventajas, así que lo redondearé. Agregaré de mí mismo que me acostumbré un poco y realmente sentí lo conveniente que es escribir código en este estilo cuando se trata de interfaces. ¡Con esto, Apple golpeó lo que se llama la diana!

¿En qué consiste Ver?


Pero echemos un vistazo más de cerca. Apple sugiere que el estilo declarativo es así:

 struct ContentView: View { var text1 = "some text" var text2 = "some more text" var body: some View { VStack{ Text(text1) .padding() .frame(width: 100, height: 50) Text(text2) .background(Color.gray) .border(Color.green) } } } 

Tenga en cuenta que View es una estructura con algunos parámetros. Para hacer la View Estructura, necesitamos establecer el body parámetro calculado, que devuelve some View . Hablaremos de esto más tarde. El contenido del body: some View { … } cierre de body: some View { … } es la descripción de lo que se mostrará en la pantalla. En realidad, esto es todo lo que se requiere para que nuestra estructura satisfaga los requisitos del protocolo View. Sugiero centrarse principalmente en el body .

Y así, estantes


En total, conté tres tipos de elementos a partir de los cuales se construye el cuerpo de la Vista:

  • Otra vista
    Es decir Cada vista contiene una o más View . Esos, a su vez, también pueden contener View predefinidas del sistema como Text() y personalizadas y complejas escritas por el desarrollador. Resulta una especie de muñeca de anidación con un nivel ilimitado de anidación.
  • Modificadores
    Con la ayuda de modificadores, ocurre toda la magia. Gracias a ellos, le decimos breve y claramente a SwiftUI qué tipo de vista queremos ver. Cómo funciona, aún lo resolveremos, pero lo principal es que los modificadores agregan la pieza requerida al contenido de una View determinada.
  • Contenedores
    Los primeros contenedores con los que comienza el estándar "Hola, mundo" son HStack y VStack . Un poco más tarde, aparece Group , Section y otros. De hecho, los contenedores son la misma Vista, pero tienen una función. Les pasa algún contenido que desea mostrar. La característica completa del contenedor es que de alguna manera debe agrupar y mostrar los elementos de este contenido. En este sentido, los contenedores son similares a los modificadores, con la única diferencia de que los modificadores están destinados a cambiar una Vista ya preparada, y los contenedores organizan esta Vista (elementos de contenido o bloques de sintaxis declarativa) en un cierto orden, por ejemplo, vertical u horizontalmente ( VStack{...} HStack{...} ). También hay contenedores específicos, como ForEach o GeometryReader , hablaremos de ellos un poco más tarde.

    En general, considero los contenedores como cualquier Vista, a la que se puede pasar el Contenido como parámetro.

Y eso es todo. Todos los elementos de pura raza SwiftUI pueden atribuirse a uno de estos tipos. Sí, esto no es suficiente para llenar su Vista con funcionalidad, pero esto es todo lo que necesita para mostrar su funcionalidad en la pantalla.

.modifiers (): ¿cómo se organizan?


Comencemos con lo más simple. Un modificador es en realidad una cosa muy simple. Simplemente toma algo de View , le aplica algunos cambios (¿o sí?) Y lo devuelve. Es decir El modificador es una función de la View sí, que se devuelve a self , habiendo realizado previamente algunas modificaciones.

A continuación se muestra un ejemplo de código con el que declaro mi propio modificador. Más precisamente, sobrecargo el frame(width:height:) modificador existente frame(width:height:) , con el que puede corregir las dimensiones específicas de una Vista en particular. Desde el cuadro correspondiente, debe especificar el ancho y el alto, y necesitaba pasarle un objeto CGSize con un argumento, que es una descripción de solo el largo y el ancho. ¿Por qué necesitaba esto? Lo diré un poco más tarde.

 struct FrameFromSize: ViewModifier{ let size: CGSize func body(content: Content) -> some View { content .frame(width: size.width, height: size.height) } } 

Con este código, hemos creado una estructura que cumple con el protocolo ViewModifier . Este protocolo requiere que la función body() se implemente en esta estructura, cuya entrada será Content , y la salida tendrá some View : el mismo tipo que el parámetro body de nuestra View (hablaremos sobre alguna Vista a continuación) . ¿Qué tipo de Content este?

Contenido + ViewBuilder = Ver


En la documentación incorporada sobre él lo dice:
`content` es un proxy para la vista que tendrá aplicado el modificador representado por` Self`.
Este es un tipo de proxy, que es un prefabricado de Vista al que se pueden aplicar modificadores. Un tipo de producto semiacabado. En realidad, el Content es un cierre de estilo declarativo que describe la estructura de la View . Por lo tanto, si llamamos a este modificador para alguna Vista, entonces todo lo que hace es obtener el cierre del body y pasarlo a la función de nuestro body , en el que agregamos nuestros cinco centavos a este cierre.

Una vez más, View es principalmente una estructura que almacena todos los parámetros necesarios para generar una imagen en la pantalla. Incluidas las instrucciones de montaje, de las cuales Content . Por lo tanto, un cierre en un estilo declarativo ( Content ) procesado usando ViewBuilder nos devuelve una Vista.

Volvamos a nuestro modificador. En principio, la declaración de la estructura FrameFromSize ya FrameFromSize suficiente para comenzar a aplicarla. Dentro del body podemos escribir así:

 RoundedRectangle(cornerRadius: 4).modifier(FrameFromSize(size: size)) 

modifier es un método de protocolo de vista que extrae el contenido de la View modificando, lo pasa a la función del cuerpo de la estructura del modificador y pasa el resultado al procesamiento de ViewBuilder o al siguiente modificador, si tenemos una cadena de modificaciones.

Pero puede hacerlo aún más conciso declarando su propio modificador como una función, ampliando así las capacidades del protocolo View.

 extension View{ func frame(_ size: CGSize) -> some View { self.modifier(FrameFromSize(size: size)) } } 

En este caso, sobrecargué el modificador existente .frame(width: height:) otra variante de los parámetros de .frame(width: height:) . Ahora, podemos usar la opción de llamar al modificador frame(size:) para cualquier View . Al final resultó que, nada complicado.

Un poco sobre errores
Por cierto, pensé que no era necesario expandir todo el protocolo, sería suficiente expandir específicamente el RoundedRectangle en mi caso, y debería haber funcionado, como me pareció a mí, pero parece que Xcode no esperaba tal descaro, y cayó con un error ininteligible " Abort trap: 6 "y una propuesta para enviar un volcado a los desarrolladores. En términos generales, en SwiftUI, las descripciones de errores hasta ahora muy a menudo no revelan completamente la causa de este error.

De la misma manera, puede crear modificadores personalizados y usarlos de la misma manera que la SwiftUI incorporada:

 RoundedRectangle(cornerRadius: 4).frame(size) 

Convenientemente, concisamente, claramente.

Me imagino una cadena de modificaciones como cuentas ensartadas en un hilo: nuestra vista. Esta analogía también es cierta en el sentido de que el orden en que se llaman las modificaciones importa.



Casi todo en SwiftUI es Ver
Por cierto, una observación interesante. Como parámetro de entrada, el fondo no acepta color, sino Ver. Es decir La clase Color no es solo una descripción del color, es una Vista completa, a la que se pueden aplicar modificadores y más. Y como fondo, en consecuencia, puede pasar otra Vista.

Modificadores: solo para modificaciones
Quizás valga la pena señalar un punto más. SwiftUI simplemente ignora los modificadores que no cambian el contenido de origen y no se los llama. Es decir No podrá realizar un desencadenador basado en el modificador que causa algunos eventos, pero no realiza ninguna acción con el contenido. Apple nos está presionando constantemente para que abandonemos algunas acciones en tiempo de ejecución al renderizar la interfaz y confíe en el estilo declarativo.

Vista fija


Anteriormente hablamos sobre en qué consiste el body , el cuerpo de la View o sus instrucciones de montaje. Volvamos a la View sí. En primer lugar, es una estructura en la que se pueden declarar algunos parámetros, y body es solo uno de ellos. Como ya dijimos, descubrir qué Content , el body es una instrucción sobre cómo ensamblar la Vista deseada, que es un cierre en un estilo declarativo. Pero, ¿qué debería devolver nuestro cierre?

alguna vista - conveniencia




Y sin problemas llegamos a una pregunta que durante mucho tiempo no pude resolver, aunque esto no me impidió escribir código de trabajo. ¿Qué es esto some View ? La documentación dice que esta descripción es un "tipo de resultado opaco", pero eso no tiene mucho sentido.

La palabra clave some es una versión "genérica" ​​de una descripción de un tipo devuelto por un cierre que no depende de otra cosa que no sea el código en sí. Es decir El resultado de acceder a la propiedad calculada del cuerpo de nuestra Vista debe ser alguna estructura que satisfaga el protocolo de Vista. Puede haber muchos de ellos: texto, imagen o tal vez alguna estructura que haya declarado. El chip completo de alguna palabra clave es declarar "genérico" que se ajusta al protocolo de visualización. Está estáticamente determinado por el código implementado dentro del cuerpo de su Vista, y Xcode es capaz de analizar este código y calcular la firma específica del valor de retorno (bueno, casi siempre) . Y algunos son solo un intento de no cargar al desarrollador con ceremonias innecesarias. Es suficiente que el desarrollador diga: "habrá algún tipo de Vista", y cuál, resuélvelo tú mismo. La clave aquí es que el tipo concreto no está determinado por los parámetros de entrada, como con el tipo genérico habitual, sino directamente por el código. Por lo tanto, arriba, genérico, cité.

Xcode debería poder identificar un tipo específico sin saber exactamente qué valores pasa a esta estructura. Es importante comprender esto: después de la compilación, la expresión Some View se reemplaza con el tipo específico de su View . Este tipo es bastante determinista y puede ser bastante complejo, por ejemplo, así: Group<TupleView<(Text, ForEach<[SomeClass], SomeClass.ID, Text>)>> .

El código de muestra se puede restaurar a partir de este tipo:

 Group{ Text(…) ForEach(…){(value: SomeClass) in Text(…) } } 

ForEach , como se puede ver en la firma de tipo, no es un ciclo de tiempo de ejecución. Esta es solo una View que se basa en una matriz de objetos SomeClass . como el identificador de un subView particular asociado con el elemento de colección, se indica el ID del elemento y para cada elemento se subView un subView tipo Text . Text y ForEach combinan en TupleView , y todo esto se coloca en Group . Hablaremos más sobre ForEach más detalle.

¿Imagina cuánto escribiría si nos viéramos obligados a describir una firma exacta como el body parámetro? Para evitar esto, se creó some palabra clave.

Resumen
some , esto es "genérico - viceversa". Obtenemos el genérico clásico desde fuera de la función, y ya conociendo el tipo específico del tipo genérico, Xcode define cómo funciona nuestra función. some- no depende de los parámetros de entrada, sino solo del código en sí. Esto es simplemente una abreviatura, que permite no definir un tipo específico, sino indicar solo la familia del valor devuelto por la función (protocolo).

alguna vista - y consecuencias


El enfoque para calcular el tipo estático de una expresión dentro del cuerpo da lugar, en mi opinión, a dos puntos importantes:

  • Cuando se compila, Xcode analiza el contenido del cuerpo para calcular el tipo de retorno específico. En cuerpos complejos, esto puede llevar algo de tiempo. En algunos cuerpos particularmente complejos, es posible que no pueda hacer frente a un tiempo sano en absoluto, y lo dirá directamente.

    En general, View debe mantenerse lo más simple posible. Las estructuras complejas se colocan mejor en Vista separada. Por lo tanto, cadenas enteras de tipos reales se reemplazan por un tipo: su CustomView, que permite que el compilador no se vuelva loco con todo este desastre.
    Por cierto, es realmente muy conveniente depurar una pequeña parte de una Vista grande, aquí mismo, sobre la marcha, recibiendo y observando el resultado en Canvas.
  • No podemos controlar directamente el flujo. Si, si no, SwiftUI aún puede procesarlo creando una "Vista de Schrödinger" de tipo <_ConditionalContent <Text, TextField >>, entonces el operador de condición trinar solo se puede usar para seleccionar un valor de parámetro específico, pero no un tipo, o incluso para seleccionar una secuencia de modificadores.

    Pero vale la pena restaurar el mismo orden de modificadores, y ese registro deja de ser un problema.

Excepto cuerpo


Sin embargo, puede haber otros parámetros en la estructura con los que puede trabajar. Como parámetros podemos declarar las siguientes cosas.

Parámetros externos


Estos son parámetros de estructura simples que debemos pasar desde el exterior durante la inicialización para que la Vista los represente de alguna manera:

 struct TextView: View { let textValue: String var body: some View { Text(textValue) } } 

En este ejemplo, el textValue para la estructura TextView es un parámetro que debe rellenarse externamente porque no tiene un valor predeterminado. Dado que las estructuras admiten la generación automática de inicializadores, podemos usar esta Vista simplemente:

  TextView(textValue: "some text") 

Desde el exterior, también puede transferir los cierres que deben realizarse cuando se produce un evento. Por ejemplo, Button(lable:action:) hace exactamente eso: realiza el cierre de la acción aprobada cuando se hace clic en un botón.

estado - parámetros


SwiftUI está utilizando de manera muy activa la nueva función Swift 5.1: Property Wrapper .

En primer lugar, estas son variables de estado: parámetros almacenados de nuestra estructura, cuyo cambio debe reflejarse en la pantalla. @State — , @ObservedObject — . ObservableObject — , (View, @ObservedObject ) . @Published .

, , ObservableObjectPublisher , willSet() , , , .

, , body — ? - State-, - State- body . , body — , , stateless . View , , body . . State- View . , , , . , body — . , , , .

Y un comentario mas
didSet willSet , - . , . , — - , .

Ejemplo de estado clásico :

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack { Button(action: {self.tapCount += 1}, label: { Text("Tap count \(tapCount)") }) } } } 

Parámetros de enlace


, - View @State @ObservedObject . View ? SwiftUI PropertyWrapper — @Binding . . View , , , , View . @State — , , . , @Binding . Property Wrapper, , , View . inout View . , , View, . inout , $, , . React .

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack{ SomeView(count: $tapCount) Text("you tap \(tapCount) times") } } } 

. @Binding var tapCount: Int , , Int ,

 Binding<Int> 

, , View .

 struct SomeView: View{ @Binding var tapCount: Int init(count: Binding<Int>){ self._tapCount = count //    -    } var body: some View{ Button(action: {self.tapCount += 1}, label: { Text("Tap me") }) } } 

, init , - @PropertyWrapper self._ — , self . , self._ . .

, , - PropertyWrapper , -,

 Binding<Int> 

Int .wrappedValue .

,
, Binding . View View. View , @Binding-. , View State @Binding — , State- Binding. -, , .

EnvironmentObject


, EnvironmentObjectBinding , View , .

 ContentView().environmentObject(session) 

, , - , View. , , - , EnvironmentObject , View. View, , , @EnvironmentObject

  @EnvironmentObject var session: Session 

. EnvironmentObject , . 3-, , , , . EnvironmentObject , View . , Binding .

@Environment — . — , .. . , , ( ), , .. , CoreData:

 @Environment(\.managedObjectContext) var moc: NSManagedObjectContext 

, CoreData SwiftUI . , , . .

Custom @PropertyWrapper


, PropertyWrapper — setter- getter-, , property wrapper . , getter{} setter{} , , View , . , PropertyWrapper UserDefaults .

 @propertyWrapper struct UserDefault<T> { var key: String var initialValue: T var wrappedValue: T { set { UserDefaults.standard.set(newValue, forKey: key) } get { UserDefaults.standard.object(forKey: key) as? T ?? initialValue } } } 

, UserDefaults . Apple , , , , .

, - ( ), , UserDefaults , :

 enum UserPreferences { @UserDefault(key: "isCheatModeEnabled", initialValue: false) static var isCheatModeEnabled: Bool @UserDefault(key: "highestScore", initialValue: 10000) static var highestScore: Int @UserDefault(key: "nickname", initialValue: "cloudstrife97") static var nickname: String } 

, , , .

 UserPreferences.isCheatModeEnabled = true UserPreferences.highestScore = 25000 UserPreferences.nickname = "squallleonhart” 

.


, , . , body . , , View . , . , — . , @ViewBuilder , View, View, ( ). , — . VStack , HStack , . , View, Content , , View . , View . , HStack{Text(…)} TupleView<Text, Image> .

, , View , — , , body. , , Text(«a») Text(«b») HStack . offset() position() , , HStack :
HStack(spacing:, alingment:, context:).
, , . — .

ForEach


Por separado, vale la pena hablar de ello ForEach. Este es un contenedor que sirve para reflejar en la pantalla todos los elementos de la colección transferida. En primer lugar, debe comprender que esto no es lo mismo que solicitar una colección forEach(…). Como dijimos anteriormente, ForEachdevuelve uno único View, creado sobre la base de los elementos de la colección transferida. Es decir , , — .
, ForEach - , , — , , , , ( List ).

ForEach : ( data: RandomAccesCollection ), ( id: Hashable ) ( content: ()->Content ). : , ForEach Content — .. . , content , ForEach , .

La colección ForEachno es adecuada para ninguno, sino solo RandomAccesCollection. Para varias colecciones desordenadas, es suficiente llamar al método sorted(by:)con el que puede obtener RandomAccesCollection.

ForEach- Este es un conjunto subViewgenerado para cada elemento de la colección basado en el contenido transmitido. Es importante tener en cuenta que SwiftUI necesita saber subViewcon cuál elemento de la colección está asociado. Para esto, cada uno Viewdebe tener un identificador. El segundo parámetro es necesario precisamente para esto. Si los elementos de la colección son Hashabletipos, como cadenas, puede escribir simplemente id: \.self. Esto significará que la cadena en sí será el identificador. Si los elementos de la colección son clases y satisfacen el protocoloIdentifiable- Entonces se puede perder el segundo argumento. En este caso, la identificación de cada elemento de la colección se convertirá en un identificador subView. Si su objeto tiene algún tipo de accesorios que brindan unicidad y que cumplan con el protocolo Hashable, puede especificarlo así:

 ForEach(values, id: \.value){item in …} 

, valuesSomeObject , value: Int . , View , . , - . View 1 1 ( ), , @Binding View .

, , Identifiable, . :
 ForEach(keys.indices){ind in SomeView(key: self.keys[ind]) } 

, , . , . , , , , , , JSON . , .

, Content , ForEach . , , (.. , 2 — ). , Groupe{} — .

. , ViewBuilder . , .frame(size:) ? . ( , ). CGSize, . , size, .ftame(width: size.width, height: size.height) — . , — .

Custom container View


, . , “1:N” . dict: [KeyObject: [SomeObject]] .

, KeyObject ( Hashable ), — — SomeObject .

 class SomeObject: Identifiable{ let value: Int public let id: UUID = UUID() init(value: Int){ self.value = value } } class KeyObject: Hashable, Comparable{ var name: String init(name: String){ self.name = name } static func < (lhs: KeyObject, rhs: KeyObject) -> Bool { lhs.name < rhs.name } static func == (lhs: KeyObject, rhs: KeyObject) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } 

Si su aplicación planea algún tipo de análisis con agrupación, tiene sentido crear un contenedor separado para mostrar dichos diccionarios para no duplicar todo el código en cada vista. Y dado que el usuario puede cambiar las agrupaciones, tendremos que usar genéricos. No lo compliqué agregando un diseño visual, dejando solo la estructura de nuestro contenedor:

 struct TreeView<K: Hashable, V: Identifiable, KeyContent, ValueContent>: View where K: Comparable, KeyContent: View, ValueContent: View{ let data: [K: [V]] let keyContent: (K)->KeyContent let valueContent: (V)->ValueContent var body: some View{ VStack(alignment: .leading, spacing: 0){ ForEach(data.keys.sorted(), id: \.self){(key: K) in VStack(alignment: .trailing, spacing: 0){ self.keyContent(key) ForEach(self.data[key]!){(value: V) in self.valueContent(value) } } } } } } 

, [K: [V]] ( K — - , V — , ), : , — . , ViewBuilder- ( ), ForEach . RandomAccessCollection , dict.keys , . Comparable KeyObject .

ForEach . , ( \.self) como el identificador de cada anidado View. Podría hacer esto porque las claves de diccionario deberían admitir el protocolo de todos modos Hashable. En el segundo caso, agregué SomeObjectsoporte de protocolo a la claseIdentifiable . — id. id . — , — id. , . .. , id. id — . — id , ForEach .

:

 struct ContentView: View { let dict: [KeyObject: [SomeObject]] = [ KeyObject(name: "1st group") : [SomeObject(value: 1), SomeObject(value: 2), SomeObject(value: 3)], KeyObject(name: "2nd group") : [SomeObject(value: 4), SomeObject(value: 5), SomeObject(value: 6)], KeyObject(name: "3rd group") : [SomeObject(value: 7), SomeObject(value: 8), SomeObject(value: 9)] ] var body: some View { TreeView(data: dict, keyContent: {keyObject in Text("the key is: \(keyObject.name)") } ){valueObject in Text("value: \(valueObject.value)") } } } 

Canvas:



to be continued


. , , CoreData SwiftUI, , , , SwiftUI , . , , .

, — . .

Source: https://habr.com/ru/post/485548/


All Articles