Redes con Combine y SwiftUI

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, @ObservedObjecty @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

  • acceder a la red,
  • datos del mapa,
  • manejar errores

… 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.

Cómo obtener datos usando URLSession

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 URLSessionse 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:

  1. No está claro de inmediato cuál es el camino feliz: la única ubicación que devuelve un resultado exitoso está bastante oculta (1), y los desarrolladores que son nuevos en el uso de controladores de finalización pueden confundirse por el hecho de que el camino feliz ni siquiera usa un returndeclaración para entregar el resultado de la llamada de red a la persona que llama.
  2. El manejo de errores está disperso por todas partes (2, 3, 4, 5).
  3. Hay varios puntos de salida y es fácil olvidar una de las returndeclaraciones en las if letcondiciones.
  4. En general, es difícil de leer y mantener, incluso si es un desarrollador Swift experimentado.
  5. Es fácil olvidar que tiene que llamar 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 resumeno 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 Networkingcarpeta. Para poder beneficiarse al máximo, también proporcioné un servidor de demostración (construido con Vapor) en la serversubcarpeta. Para ejecutarlo en su máquina, haga lo siguiente:

Cómo obtener datos usando Combine

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 guarddeclaración que asegura que tenemos una URL válida) solo hay un punto de salida.

Veamos el código paso a paso:

  1. Utilizamos dataTaskPublisherpara 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.
  2. Una vez que se devuelve la solicitud, el publicador emite un valor que contiene tanto el datacomo el response. En esta línea, usamos el mapoperador 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:
  3. En lugar de llamar al completioncierre, podemos devolver un Booleanvalor para indicar si el nombre de usuario todavía está disponible o no. Este valor se pasará por la canalización.
  4. En caso de que falle la asignación de datos, detectamos el error y simplemente devolvemos false, lo que parece ser un buen compromiso.
  5. Hacemos lo mismo para cualquier error que pueda ocurrir al acceder a la red. Esta es una simplificación que podríamos necesitar revisar en el futuro.

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:

Destrucción de tuplas usando rutas clave

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 datay el responsede 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 mapoperador 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) 

Mapear datos más fácilmente

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 datavalor del editor ascendente y lo decodificará en una UserNameAvailableMessageinstancia.

Y finalmente, podemos usar el mapoperador nuevamente para desestructurar UserNameAvailableMessagey acceder a su isAvailableatributo:

return URLSession.shared.dataTaskPublisher(for: url)
  .map(\.data)
  .decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
  .map(\.isAvailable)

Obteniendo datos usando Combine, simplificado

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()
}

Cómo conectarse a SwiftUI

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 Textetiqueta 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 @Publishedque 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 checkUserNameAvailablecanalizació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 flatMapoperador. Esto tomará todos los eventos de entrada de un editor ascendente (es decir, los valores emitidos por la $usernamepropiedad publicada) y los transformará en un nuevo editor (en nuestro caso, el editor checkUserNameAvailableen ).

En el siguiente y último paso, conectaremos el resultado de isUsernameAvailablePublisherla 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 isValidy usernameMessageal 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 isUsernameAvailablePublishery usarlo para controlar el isValidestado 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.

Cómo manejar "No se permite publicar cambios desde hilos en segundo plano"

Cuando ejecute este código, notará un par de problemas:

  1. El punto final de la API se llama varias veces por cada carácter que escribe
  2. Xcode le dice que no debe actualizar la interfaz de usuario desde un hilo de fondo

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.

Cierre

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é isUsernameAvailablePublisherusa Nevercomo 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

#swiftui 

What is GEEK

Buddha Community

Redes con Combine y SwiftUI

Redes con Combine y SwiftUI

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, @ObservedObjecty @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

  • acceder a la red,
  • datos del mapa,
  • manejar errores

… 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.

Cómo obtener datos usando URLSession

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 URLSessionse 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:

  1. No está claro de inmediato cuál es el camino feliz: la única ubicación que devuelve un resultado exitoso está bastante oculta (1), y los desarrolladores que son nuevos en el uso de controladores de finalización pueden confundirse por el hecho de que el camino feliz ni siquiera usa un returndeclaración para entregar el resultado de la llamada de red a la persona que llama.
  2. El manejo de errores está disperso por todas partes (2, 3, 4, 5).
  3. Hay varios puntos de salida y es fácil olvidar una de las returndeclaraciones en las if letcondiciones.
  4. En general, es difícil de leer y mantener, incluso si es un desarrollador Swift experimentado.
  5. Es fácil olvidar que tiene que llamar 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 resumeno 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 Networkingcarpeta. Para poder beneficiarse al máximo, también proporcioné un servidor de demostración (construido con Vapor) en la serversubcarpeta. Para ejecutarlo en su máquina, haga lo siguiente:

Cómo obtener datos usando Combine

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 guarddeclaración que asegura que tenemos una URL válida) solo hay un punto de salida.

Veamos el código paso a paso:

  1. Utilizamos dataTaskPublisherpara 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.
  2. Una vez que se devuelve la solicitud, el publicador emite un valor que contiene tanto el datacomo el response. En esta línea, usamos el mapoperador 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:
  3. En lugar de llamar al completioncierre, podemos devolver un Booleanvalor para indicar si el nombre de usuario todavía está disponible o no. Este valor se pasará por la canalización.
  4. En caso de que falle la asignación de datos, detectamos el error y simplemente devolvemos false, lo que parece ser un buen compromiso.
  5. Hacemos lo mismo para cualquier error que pueda ocurrir al acceder a la red. Esta es una simplificación que podríamos necesitar revisar en el futuro.

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:

Destrucción de tuplas usando rutas clave

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 datay el responsede 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 mapoperador 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) 

Mapear datos más fácilmente

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 datavalor del editor ascendente y lo decodificará en una UserNameAvailableMessageinstancia.

Y finalmente, podemos usar el mapoperador nuevamente para desestructurar UserNameAvailableMessagey acceder a su isAvailableatributo:

return URLSession.shared.dataTaskPublisher(for: url)
  .map(\.data)
  .decode(type: UserNameAvailableMessage.self, decoder: JSONDecoder())
  .map(\.isAvailable)

Obteniendo datos usando Combine, simplificado

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()
}

Cómo conectarse a SwiftUI

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 Textetiqueta 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 @Publishedque 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 checkUserNameAvailablecanalizació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 flatMapoperador. Esto tomará todos los eventos de entrada de un editor ascendente (es decir, los valores emitidos por la $usernamepropiedad publicada) y los transformará en un nuevo editor (en nuestro caso, el editor checkUserNameAvailableen ).

En el siguiente y último paso, conectaremos el resultado de isUsernameAvailablePublisherla 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 isValidy usernameMessageal 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 isUsernameAvailablePublishery usarlo para controlar el isValidestado 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.

Cómo manejar "No se permite publicar cambios desde hilos en segundo plano"

Cuando ejecute este código, notará un par de problemas:

  1. El punto final de la API se llama varias veces por cada carácter que escribe
  2. Xcode le dice que no debe actualizar la interfaz de usuario desde un hilo de fondo

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.

Cierre

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é isUsernameAvailablePublisherusa Nevercomo 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

#swiftui 

Shawn  Durgan

Shawn Durgan

1593196500

Introduction to MVVM with SwiftUI and Combine

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

  • Model: It represents your domain model and contains all the business logic. It will not have any knowledge on how the view would be presented to user.
  • View: This is passive and doesn’t have any knowledge on business. It’s just a visual representation of ViewModel.
  • View Model: It represents a State of the View at any given point of time. It will also contain the Presentation logic. ViewModel transforms the Model in a way that view can consume directly. When there’s a change in the model, ViewModel informs View about the change, mostly through binding.

Let’s take a simple example of Stopwatch to architect our SwiftUI app with MVVM.

UI consist of:

  1. Text to show Time Elapsed.
  2. Below the Text we have two buttons. One to Start/Stop the Stopwatch and the other to record the Lap time.
  3. We also have a List to display all the recorded Lap times.

#combine #architecture #mvvm #swiftui #ios

Lulu  Hegmann

Lulu Hegmann

1591852200

Building an iOS app using SwiftUI + Combine + MVVM [Part 2]

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

SwiftUI Scratch Card Effect - Custom Masking - Animation's -View Builder-SwiftUI Tutorials

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

SwiftUI Cloud App UI - Adaptable For Both iOS & macOS - Complex UI - SwiftUI Tutorials

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