1642936020
No mantener la interfaz de usuario actualizada en las diferentes partes de una aplicación puede resultar en una experiencia de usuario exasperantemente mala, y estoy seguro de que todos tenemos en mente al menos una o dos aplicaciones que son notorias por este tipo de comportamiento.
Tradicionalmente, escribir aplicaciones que mantengan el estado sincronizado en la interfaz de usuario y el modelo de datos subyacente ha sido una tarea difícil, y la comunidad de desarrollo ha ideado muchos enfoques para abordar este desafío de maneras más o menos amigables para los desarrolladores.
La programación reactiva es uno de esos enfoques, y la administración de estado reactivo de SwiftUI lo hace mucho más fácil al presentar la noción de una fuente de verdad que se puede compartir en su aplicación usando los contenedores de propiedad de SwiftUI como @EnvironmentObject
, @ObservedObject
y @StateObject
.
Esta fuente de verdad suele ser su modelo de datos en memoria, pero como todos sabemos, ninguna aplicación existe de forma aislada. La mayoría de las aplicaciones modernas necesitan acceder a la red (u otros servicios) en algún momento, y esto significa introducir un comportamiento asíncrono en su aplicación. Hay muchas maneras de lidiar con el comportamiento asíncrono en nuestras aplicaciones: métodos delegados, controladores de devolución de llamada, Combine y async/await, por nombrar solo algunos.
En esta serie, veremos cómo usar Combine en el contexto de SwiftUI para
… y lidiar con algunos escenarios avanzados.
Comencemos analizando cómo usar Combine para obtener datos de un servidor y asignar el resultado a un Swift struct
.
Supongamos que estamos trabajando en una pantalla de registro para una aplicación y uno de los requisitos es verificar si el nombre de usuario que eligió el usuario todavía está disponible en nuestra base de datos de usuarios. Esto requiere que nos comuniquemos con nuestro servidor de autorización. Aquí hay una solicitud que muestra cómo podríamos intentar averiguar si el nombre de usuario sjobs todavía está disponible:
GET localhost:8080/isUserNameAvailable?userName=sjobs HTTP/1.1
Luego, el servidor respondería con un breve documento JSON que indica si el nombre de usuario todavía está disponible:
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 39
connection: close
date: Thu, 06 Jan 2022 16:09:08 GMT
{"isAvailable":false, "userName":"sjobs"}
Para realizar esta solicitud en Swift, podemos usar URLSession
. La forma tradicional de obtener datos de la red URLSession
se ve así:
func checkUserNameAvailableOldSchool(userName: String, completion: @escaping (Result<Bool, NetworkError>) -> Void) {
guard let url = URL(string: "http://127.0.0.1:8080/isUserNameAvailable?userName=\(userName)") else { // 2
completion(.failure(.invalidRequestError("URL invalid")))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { // 3
completion(.failure(.transportError(error)))
return
}
if let response = response as? HTTPURLResponse, !(200...299).contains(response.statusCode) { // 4
completion(.failure(.serverError(statusCode: response.statusCode)))
return
}
guard let data = data else { // 5
completion(.failure(.noData))
return
}
do {
let decoder = JSONDecoder()
let userAvailableMessage = try decoder.decode(UserNameAvailableMessage.self, from: data)
completion(.success(userAvailableMessage.isAvailable)) // 1
}
catch {
completion(.failure(.decodingError(error)))
}
}
task.resume() // 6
}
Y aunque este código funciona bien y no tiene ningún problema inherente, tiene una serie de problemas:
return
declaración para entregar el resultado de la llamada de red a la persona que llama.return
declaraciones en las if let
condiciones.resume()
para realizar la solicitud (6). Estoy bastante seguro de que la mayoría de nosotros hemos estado buscando frenéticamente errores, solo para descubrir que olvidamos iniciar la solicitud usando resume
. Y sí, creo que resume
no es un gran nombre para una API que está destinada a enviar la solicitud.Ejecutando los ejemplos de código
Encontrará todos los ejemplos de código en el repositorio de GitHub adjunto , en la
Networking
carpeta. Para poder beneficiarse al máximo, también proporcioné un servidor de demostración (construido con Vapor) en laserver
subcarpeta. Para ejecutarlo en su máquina, haga lo siguiente:
Cuando presentaron Combine, Apple agregó editores para muchas de sus propias API asincrónicas. Esto es genial, ya que nos facilita usarlos en nuestras propias canalizaciones de Combine.
Ahora, echemos un vistazo a cómo se ve el código después de refactorizarlo para usar Combine.
func checkUserNameAvailable(userName: String) -> AnyPublisher<Bool, Never> {
guard let url = URL(string: "http://127.0.0.1:8080/isUserNameAvailable?userName=\(userName)") else {
return Just(false).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url) // 1
.map { data, response in // 2
do {
let decoder = JSONDecoder()
let userAvailableMessage = try decoder.decode(UserNameAvailableMessage.self, from: data)
return userAvailableMessage.isAvailable // 3
}
catch {
return false // 4
}
}
.replaceError(with: false) // 5
.eraseToAnyPublisher()
}
Esto ya es mucho más fácil de leer y (a excepción de la guard
declaración que asegura que tenemos una URL válida) solo hay un punto de salida.
Veamos el código paso a paso:
dataTaskPublisher
para realizar la solicitud. Este editor es un editor único y emitirá un evento una vez que hayan llegado los datos solicitados. Vale la pena tener en cuenta que los editores de Combine no realizan ningún trabajo si no hay suscriptores. Esto significa que este editor no realizará ninguna llamada a la URL dada a menos que haya al menos un suscriptor. Más adelante le mostraré cómo conectar esta canalización a la interfaz de usuario y asegurarme de que se llame cada vez que el usuario ingrese su nombre de usuario preferido.data
como el response
. En esta línea, usamos el map
operador para transformar este resultado. Como puede ver, podemos reutilizar la mayor parte del código de mapeo de datos de la versión anterior del código, excepto por un par de pequeños cambios:completion
cierre, podemos devolver un Boolean
valor para indicar si el nombre de usuario todavía está disponible o no. Este valor se pasará por la canalización.false
, lo que parece ser un buen compromiso.Esto se ve mucho mejor y más fácil de leer que la versión inicial, y podríamos detenernos aquí e integrar esto en nuestra aplicación.
Pero lo podemos hacer mejor. Aquí hay tres cambios que harán que el código sea más lineal y más fácil de razonar:
A menudo nos encontramos en una situación en la que necesitamos extraer un atributo específico de una variable. En nuestro ejemplo, recibimos una tupla que contiene el data
y el response
de la solicitud de URL que enviamos. Aquí está la declaración respectiva en URLSession
:
public struct DataTaskPublisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = (data: Data, response: URLResponse)
...
}
Combine proporciona una versión sobrecargada del map
operador que nos permite desestructurar la tupla utilizando una ruta clave y acceder solo al atributo que nos interesa:
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
Dado que el mapeo de datos es una tarea tan común, Combine viene con un operador dedicado para hacerlo más fácil: decode(type:decoder:)
.
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
Esto devolverá decodificar el data
valor del editor ascendente y lo decodificará en una UserNameAvailableMessage
instancia.
Y finalmente, podemos usar el map
operador nuevamente para desestructurar UserNameAvailableMessage
y acceder a su isAvailable
atributo:
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
.map(\.isAvailable)
Con todos estos cambios implementados, ahora tenemos una versión de la canalización que es fácil de leer y tiene un flujo lineal:
func checkUserNameAvailable(userName: String) -> AnyPublisher<Bool, Never> {
guard let url = URL(string: "http://127.0.0.1:8080/isUserNameAvailable?userName=\(userName)") else {
return Just(false).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
.map(\.isAvailable)
.replaceError(with: false)
.eraseToAnyPublisher()
}
Terminemos viendo cómo integrar esta nueva canalización Combine en nuestro formulario de registro hipotético.
Aquí hay una versión resumida de un formulario de registro que contiene solo un campo de nombre de usuario, una Text
etiqueta para mostrar un mensaje y un botón de registro. En una aplicación real, también tendríamos algunos elementos de la interfaz de usuario para proporcionar una contraseña y una confirmación de contraseña.
Todos los elementos de la interfaz de usuario están conectados a un modelo de vista para separar las preocupaciones y mantener la vista limpia y fácil de leer:
struct SignUpScreen: View {
@StateObject private var viewModel = SignUpScreenViewModel()
var body: some View {
Form {
// Username
Section {
TextField("Username", text: $viewModel.username)
.autocapitalization(.none)
.disableAutocorrection(true)
} footer: {
Text(viewModel.usernameMessage)
.foregroundColor(.red)
}
// Submit button
Section {
Button("Sign up") {
print("Signing up as \(viewModel.username)")
}
.disabled(!viewModel.isValid)
}
}
}
}
Dado @Published
que las propiedades son editores de Combine, podemos suscribirnos a ellas para recibir actualizaciones cada vez que cambie su valor. Esto nos permite llamar a la checkUserNameAvailable
canalización que creamos anteriormente.
Vamos a crear un editor reutilizable que podamos usar para controlar las partes de nuestra interfaz de usuario que necesitan mostrar información que depende de si el nombre de usuario está disponible o no. Una forma de hacer esto es crear una propiedad computada diferida. Esto asegura que la canalización solo se configurará una vez que se necesite, y que solo habrá una instancia de la canalización.
class SignUpScreenViewModel: ObservableObject {
// MARK: Input
@Published var username: String = ""
// MARK: Output
@Published var usernameMessage: String = ""
@Published var isValid: Bool = false
...
}
Para llamar a otra tubería y luego usar su resultado, podemos usar el flatMap
operador. Esto tomará todos los eventos de entrada de un editor ascendente (es decir, los valores emitidos por la $username
propiedad publicada) y los transformará en un nuevo editor (en nuestro caso, el editor checkUserNameAvailable
en ).
En el siguiente y último paso, conectaremos el resultado de isUsernameAvailablePublisher
la interfaz de usuario. Si observa el modelo de vista, notará que tenemos dos propiedades en la sección de salida del modelo de vista: una para cualquier mensaje relacionado con el nombre de usuario y otra que contiene el estado de validación general del formulario ( recuerde, en un formulario de registro real, es posible que también debamos validar los campos de contraseña).
Los editores combinados se pueden conectar a más de un suscriptor, por lo que podemos conectarnos a ambos isValid
y usernameMessage
al isUsernameAvailablePublisher
:
class SignUpScreenViewModel: ObservableObject {
...
init() {
isUsernameAvailablePublisher
.assign(to: &$isValid)
isUsernameAvailablePublisher
.map { $0 ? "" : "Username not available. Try a different one."}
.assign(to: &$usernameMessage)
}
}
El uso de este enfoque nos permite reutilizar isUsernameAvailablePublisher
y usarlo para controlar el isValid
estado general del formulario (que activará/desactivará el botón Enviar ) y la etiqueta del mensaje de error que informa al usuario si su nombre de usuario elegido todavía está disponible o no.
Cuando ejecute este código, notará un par de problemas:
Profundizaremos en las razones de estos problemas en los próximos episodios, pero por ahora, abordemos este mensaje de error:
SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
El motivo de este mensaje de error es que Combine ejecutará la solicitud de red en un subproceso en segundo plano. Cuando se cumple la solicitud, asignamos el resultado a una de las propiedades publicadas en el modelo de vista. Esto, a su vez, hará que SwiftUI actualice la interfaz de usuario, y esto sucederá en el subproceso de primer plano.
Para evitar que esto suceda, debemos indicarle a Combine que cambie al subproceso en primer plano una vez que haya recibido el resultado de la solicitud de red, utilizando el receive(on:)
operador:
private lazy var isUsernameAvailablePublisher: AnyPublisher<Bool, Never> = {
$username
.flatMap { username -> AnyPublisher<Bool, Never> in
self.authenticationService.checkUserNameAvailableNaive(userName: username)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
Profundizaremos en la creación de subprocesos en uno de los próximos episodios cuando hablemos de los planificadores de Combine.
En esta publicación, le mostré cómo acceder a la red usando Combine, y cómo esto le permite escribir un código de línea recta que debería ser más fácil de leer y mantener que la respectiva contraparte impulsada por devolución de llamada.
Ahora, es posible que se pregunte por qué isUsernameAvailablePublisher
usa Never
como tipo de error; después de todo, los errores de red son algo con lo que debemos lidiar.
Veremos el manejo de errores (y el mapeo de datos personalizados) en uno de los próximos episodios. También buscaremos formas de optimizar nuestra capa de red basada en Combine, ¡así que estad atentos!
Gracias por leer 🔥
Enlace: https://betterprogramming.pub/networking-with-combine-and-swiftui-fdf8182f7360
1642936020
No mantener la interfaz de usuario actualizada en las diferentes partes de una aplicación puede resultar en una experiencia de usuario exasperantemente mala, y estoy seguro de que todos tenemos en mente al menos una o dos aplicaciones que son notorias por este tipo de comportamiento.
Tradicionalmente, escribir aplicaciones que mantengan el estado sincronizado en la interfaz de usuario y el modelo de datos subyacente ha sido una tarea difícil, y la comunidad de desarrollo ha ideado muchos enfoques para abordar este desafío de maneras más o menos amigables para los desarrolladores.
La programación reactiva es uno de esos enfoques, y la administración de estado reactivo de SwiftUI lo hace mucho más fácil al presentar la noción de una fuente de verdad que se puede compartir en su aplicación usando los contenedores de propiedad de SwiftUI como @EnvironmentObject
, @ObservedObject
y @StateObject
.
Esta fuente de verdad suele ser su modelo de datos en memoria, pero como todos sabemos, ninguna aplicación existe de forma aislada. La mayoría de las aplicaciones modernas necesitan acceder a la red (u otros servicios) en algún momento, y esto significa introducir un comportamiento asíncrono en su aplicación. Hay muchas maneras de lidiar con el comportamiento asíncrono en nuestras aplicaciones: métodos delegados, controladores de devolución de llamada, Combine y async/await, por nombrar solo algunos.
En esta serie, veremos cómo usar Combine en el contexto de SwiftUI para
… y lidiar con algunos escenarios avanzados.
Comencemos analizando cómo usar Combine para obtener datos de un servidor y asignar el resultado a un Swift struct
.
Supongamos que estamos trabajando en una pantalla de registro para una aplicación y uno de los requisitos es verificar si el nombre de usuario que eligió el usuario todavía está disponible en nuestra base de datos de usuarios. Esto requiere que nos comuniquemos con nuestro servidor de autorización. Aquí hay una solicitud que muestra cómo podríamos intentar averiguar si el nombre de usuario sjobs todavía está disponible:
GET localhost:8080/isUserNameAvailable?userName=sjobs HTTP/1.1
Luego, el servidor respondería con un breve documento JSON que indica si el nombre de usuario todavía está disponible:
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 39
connection: close
date: Thu, 06 Jan 2022 16:09:08 GMT
{"isAvailable":false, "userName":"sjobs"}
Para realizar esta solicitud en Swift, podemos usar URLSession
. La forma tradicional de obtener datos de la red URLSession
se ve así:
func checkUserNameAvailableOldSchool(userName: String, completion: @escaping (Result<Bool, NetworkError>) -> Void) {
guard let url = URL(string: "http://127.0.0.1:8080/isUserNameAvailable?userName=\(userName)") else { // 2
completion(.failure(.invalidRequestError("URL invalid")))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { // 3
completion(.failure(.transportError(error)))
return
}
if let response = response as? HTTPURLResponse, !(200...299).contains(response.statusCode) { // 4
completion(.failure(.serverError(statusCode: response.statusCode)))
return
}
guard let data = data else { // 5
completion(.failure(.noData))
return
}
do {
let decoder = JSONDecoder()
let userAvailableMessage = try decoder.decode(UserNameAvailableMessage.self, from: data)
completion(.success(userAvailableMessage.isAvailable)) // 1
}
catch {
completion(.failure(.decodingError(error)))
}
}
task.resume() // 6
}
Y aunque este código funciona bien y no tiene ningún problema inherente, tiene una serie de problemas:
return
declaración para entregar el resultado de la llamada de red a la persona que llama.return
declaraciones en las if let
condiciones.resume()
para realizar la solicitud (6). Estoy bastante seguro de que la mayoría de nosotros hemos estado buscando frenéticamente errores, solo para descubrir que olvidamos iniciar la solicitud usando resume
. Y sí, creo que resume
no es un gran nombre para una API que está destinada a enviar la solicitud.Ejecutando los ejemplos de código
Encontrará todos los ejemplos de código en el repositorio de GitHub adjunto , en la
Networking
carpeta. Para poder beneficiarse al máximo, también proporcioné un servidor de demostración (construido con Vapor) en laserver
subcarpeta. Para ejecutarlo en su máquina, haga lo siguiente:
Cuando presentaron Combine, Apple agregó editores para muchas de sus propias API asincrónicas. Esto es genial, ya que nos facilita usarlos en nuestras propias canalizaciones de Combine.
Ahora, echemos un vistazo a cómo se ve el código después de refactorizarlo para usar Combine.
func checkUserNameAvailable(userName: String) -> AnyPublisher<Bool, Never> {
guard let url = URL(string: "http://127.0.0.1:8080/isUserNameAvailable?userName=\(userName)") else {
return Just(false).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url) // 1
.map { data, response in // 2
do {
let decoder = JSONDecoder()
let userAvailableMessage = try decoder.decode(UserNameAvailableMessage.self, from: data)
return userAvailableMessage.isAvailable // 3
}
catch {
return false // 4
}
}
.replaceError(with: false) // 5
.eraseToAnyPublisher()
}
Esto ya es mucho más fácil de leer y (a excepción de la guard
declaración que asegura que tenemos una URL válida) solo hay un punto de salida.
Veamos el código paso a paso:
dataTaskPublisher
para realizar la solicitud. Este editor es un editor único y emitirá un evento una vez que hayan llegado los datos solicitados. Vale la pena tener en cuenta que los editores de Combine no realizan ningún trabajo si no hay suscriptores. Esto significa que este editor no realizará ninguna llamada a la URL dada a menos que haya al menos un suscriptor. Más adelante le mostraré cómo conectar esta canalización a la interfaz de usuario y asegurarme de que se llame cada vez que el usuario ingrese su nombre de usuario preferido.data
como el response
. En esta línea, usamos el map
operador para transformar este resultado. Como puede ver, podemos reutilizar la mayor parte del código de mapeo de datos de la versión anterior del código, excepto por un par de pequeños cambios:completion
cierre, podemos devolver un Boolean
valor para indicar si el nombre de usuario todavía está disponible o no. Este valor se pasará por la canalización.false
, lo que parece ser un buen compromiso.Esto se ve mucho mejor y más fácil de leer que la versión inicial, y podríamos detenernos aquí e integrar esto en nuestra aplicación.
Pero lo podemos hacer mejor. Aquí hay tres cambios que harán que el código sea más lineal y más fácil de razonar:
A menudo nos encontramos en una situación en la que necesitamos extraer un atributo específico de una variable. En nuestro ejemplo, recibimos una tupla que contiene el data
y el response
de la solicitud de URL que enviamos. Aquí está la declaración respectiva en URLSession
:
public struct DataTaskPublisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = (data: Data, response: URLResponse)
...
}
Combine proporciona una versión sobrecargada del map
operador que nos permite desestructurar la tupla utilizando una ruta clave y acceder solo al atributo que nos interesa:
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
Dado que el mapeo de datos es una tarea tan común, Combine viene con un operador dedicado para hacerlo más fácil: decode(type:decoder:)
.
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
Esto devolverá decodificar el data
valor del editor ascendente y lo decodificará en una UserNameAvailableMessage
instancia.
Y finalmente, podemos usar el map
operador nuevamente para desestructurar UserNameAvailableMessage
y acceder a su isAvailable
atributo:
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
.map(\.isAvailable)
Con todos estos cambios implementados, ahora tenemos una versión de la canalización que es fácil de leer y tiene un flujo lineal:
func checkUserNameAvailable(userName: String) -> AnyPublisher<Bool, Never> {
guard let url = URL(string: "http://127.0.0.1:8080/isUserNameAvailable?userName=\(userName)") else {
return Just(false).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
.map(\.isAvailable)
.replaceError(with: false)
.eraseToAnyPublisher()
}
Terminemos viendo cómo integrar esta nueva canalización Combine en nuestro formulario de registro hipotético.
Aquí hay una versión resumida de un formulario de registro que contiene solo un campo de nombre de usuario, una Text
etiqueta para mostrar un mensaje y un botón de registro. En una aplicación real, también tendríamos algunos elementos de la interfaz de usuario para proporcionar una contraseña y una confirmación de contraseña.
Todos los elementos de la interfaz de usuario están conectados a un modelo de vista para separar las preocupaciones y mantener la vista limpia y fácil de leer:
struct SignUpScreen: View {
@StateObject private var viewModel = SignUpScreenViewModel()
var body: some View {
Form {
// Username
Section {
TextField("Username", text: $viewModel.username)
.autocapitalization(.none)
.disableAutocorrection(true)
} footer: {
Text(viewModel.usernameMessage)
.foregroundColor(.red)
}
// Submit button
Section {
Button("Sign up") {
print("Signing up as \(viewModel.username)")
}
.disabled(!viewModel.isValid)
}
}
}
}
Dado @Published
que las propiedades son editores de Combine, podemos suscribirnos a ellas para recibir actualizaciones cada vez que cambie su valor. Esto nos permite llamar a la checkUserNameAvailable
canalización que creamos anteriormente.
Vamos a crear un editor reutilizable que podamos usar para controlar las partes de nuestra interfaz de usuario que necesitan mostrar información que depende de si el nombre de usuario está disponible o no. Una forma de hacer esto es crear una propiedad computada diferida. Esto asegura que la canalización solo se configurará una vez que se necesite, y que solo habrá una instancia de la canalización.
class SignUpScreenViewModel: ObservableObject {
// MARK: Input
@Published var username: String = ""
// MARK: Output
@Published var usernameMessage: String = ""
@Published var isValid: Bool = false
...
}
Para llamar a otra tubería y luego usar su resultado, podemos usar el flatMap
operador. Esto tomará todos los eventos de entrada de un editor ascendente (es decir, los valores emitidos por la $username
propiedad publicada) y los transformará en un nuevo editor (en nuestro caso, el editor checkUserNameAvailable
en ).
En el siguiente y último paso, conectaremos el resultado de isUsernameAvailablePublisher
la interfaz de usuario. Si observa el modelo de vista, notará que tenemos dos propiedades en la sección de salida del modelo de vista: una para cualquier mensaje relacionado con el nombre de usuario y otra que contiene el estado de validación general del formulario ( recuerde, en un formulario de registro real, es posible que también debamos validar los campos de contraseña).
Los editores combinados se pueden conectar a más de un suscriptor, por lo que podemos conectarnos a ambos isValid
y usernameMessage
al isUsernameAvailablePublisher
:
class SignUpScreenViewModel: ObservableObject {
...
init() {
isUsernameAvailablePublisher
.assign(to: &$isValid)
isUsernameAvailablePublisher
.map { $0 ? "" : "Username not available. Try a different one."}
.assign(to: &$usernameMessage)
}
}
El uso de este enfoque nos permite reutilizar isUsernameAvailablePublisher
y usarlo para controlar el isValid
estado general del formulario (que activará/desactivará el botón Enviar ) y la etiqueta del mensaje de error que informa al usuario si su nombre de usuario elegido todavía está disponible o no.
Cuando ejecute este código, notará un par de problemas:
Profundizaremos en las razones de estos problemas en los próximos episodios, pero por ahora, abordemos este mensaje de error:
SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
El motivo de este mensaje de error es que Combine ejecutará la solicitud de red en un subproceso en segundo plano. Cuando se cumple la solicitud, asignamos el resultado a una de las propiedades publicadas en el modelo de vista. Esto, a su vez, hará que SwiftUI actualice la interfaz de usuario, y esto sucederá en el subproceso de primer plano.
Para evitar que esto suceda, debemos indicarle a Combine que cambie al subproceso en primer plano una vez que haya recibido el resultado de la solicitud de red, utilizando el receive(on:)
operador:
private lazy var isUsernameAvailablePublisher: AnyPublisher<Bool, Never> = {
$username
.flatMap { username -> AnyPublisher<Bool, Never> in
self.authenticationService.checkUserNameAvailableNaive(userName: username)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
Profundizaremos en la creación de subprocesos en uno de los próximos episodios cuando hablemos de los planificadores de Combine.
En esta publicación, le mostré cómo acceder a la red usando Combine, y cómo esto le permite escribir un código de línea recta que debería ser más fácil de leer y mantener que la respectiva contraparte impulsada por devolución de llamada.
Ahora, es posible que se pregunte por qué isUsernameAvailablePublisher
usa Never
como tipo de error; después de todo, los errores de red son algo con lo que debemos lidiar.
Veremos el manejo de errores (y el mapeo de datos personalizados) en uno de los próximos episodios. También buscaremos formas de optimizar nuestra capa de red basada en Combine, ¡así que estad atentos!
Gracias por leer 🔥
Enlace: https://betterprogramming.pub/networking-with-combine-and-swiftui-fdf8182f7360
1593196500
Over many years iOS Engineers have explored and experimented different architectural styles like MVC, MVVM, VIP, VIPER and many more. After 11 long years, Apple have decided to move away from an Event-Driven, Imperative UIKit to a State-Driven, Declarative SwiftUI. With SwiftUI’s State driven characteristic, along with Reactive Combine framework, MVVM fits in naturally as an architectural pattern.
Core components in MVVM
Let’s take a simple example of Stopwatch to architect our SwiftUI app with MVVM.
UI consist of:
#combine #architecture #mvvm #swiftui #ios
1591852200
This article guides you through building a complete app using only and exclusively those frameworks. Not only that, but we will also use a design pattern that has been gaining more and more traction in the Apple devs community.
#swiftui #combine #apple #ios #swift
1625495640
Hello Guys 🖐🖐🖐🖐
In this Video I’m going to show how to create a Stylish Scratch Card Animation Effect With Custom Masking in SwiftUI | Scratch to reveal content SwiftUI | SwiftUI Custom View Masking | SwiftUI Custom Animation’s | SwiftUI View Builder’s | SwiftUI Gesture’s | Xcode 12 SwiftUI.
► Source Code: https://www.patreon.com/posts/early-access-52075157
► Support Us
Patreon : https://www.patreon.com/kavsoft
Contributions : https://donorbox.org/kavsoft
Or By Visiting the Link Given Below:
► Kite is a free AI-powered coding assistant that will help you code faster and smarter. The Kite plugin integrates with all the top editors and IDEs to give you smart completions and documentation while you’re typing. It’s gives a great experience and I think you should give it a try too https://www.kite.com/get-kite/?utm_medium=referral&utm_source=youtube&utm_campaign=kavsoft&utm_content=description-only
► My MacBook Specs
M1 MacBook Pro(16GB)
Xcode Version: 12.5
macOS Version: 11.4 Big Sur
► Official Website: https://kavsoft.dev
For Any Queries: https://kavsoft.dev/#contact
► Social Platforms
Instagram: https://www.instagram.com/_kavsoft/
Twitter: https://twitter.com/_Kavsoft
► Timestamps
0:00 Intro
0:26 Building Home View
1:56 Building Scratch Card View(View Builder)
Thanks for watching
Make sure to like and Subscribe For More Content !!!
#swiftui #animation's #swiftui
1625488380
Hello Guys 🖐🖐🖐🖐
In this Video I’m going to show how to create a adaptable Cloud App UI for Both iOS & macOS Using SwiftUI | SwiftUI Cloud App UI | SwiftUI macOS App Development | SwiftUI Mac Catalyst Apps | SwiftUI Complex UI | SwiftUI Mac App | SwiftUI Custom Side Bar Menu | SwiftUI Hamburger Menu | SwiftUI Slide Out Menu | Xcode SwiftUI.
► Source Code: https://www.patreon.com/posts/early-access-app-52186632
► Support Us
Patreon : https://www.patreon.com/kavsoft
Contributions : https://donorbox.org/kavsoft
Or By Visiting the Link Given Below:
► Kite is a free AI-powered coding assistant that will help you code faster and smarter. The Kite plugin integrates with all the top editors and IDEs to give you smart completions and documentation while you’re typing. It’s gives a great experience and I think you should give it a try too https://www.kite.com/get-kite/?utm_medium=referral&utm_source=youtube&utm_campaign=kavsoft&utm_content=description-only
► My MacBook Specs
M1 MacBook Pro(16GB)
Xcode Version: 12.5
macOS Version: 11.4 Big Sur
► Official Website: https://kavsoft.dev
For Any Queries: https://kavsoft.dev/#contact
► Social Platforms
Instagram: https://www.instagram.com/_kavsoft/
Twitter: https://twitter.com/_Kavsoft
► Timestamps
0:00 Intro
0:42 Building Side Bar Menu
7:54 Building Main Content View
20:32 Building Side View
23:37 Adapting App For iOS
Thanks for watching
Make sure to like and Subscribe For More Content !!!
#swiftui #complex ui #swiftui #ios & macos