Indicador de actividad en SwiftUI

90

Intentando agregar un indicador de actividad de pantalla completa en SwiftUI.

Puedo usar la .overlay(overlay: )función en ViewProtocolo.

Con esto, puedo hacer cualquier vista superpuesta, pero no puedo encontrar el estilo predeterminado de iOS UIActivityIndicatorViewequivalente en SwiftUI.

¿Cómo puedo hacer un spinner de estilo predeterminado SwiftUI?

NOTA: No se trata de agregar un indicador de actividad en el marco UIKit.

Johnykutty
fuente
Intenté encontrarlo también, y fallé, supongo que se agregará más tarde :)
Markicevic
Asegúrese de presentar un problema de comentarios a Apple mediante el Asistente de comentarios. Obtener solicitudes al principio del proceso beta es la mejor manera de ver lo que desea en el marco.
Jon Shier
Puede encontrar una versión estándar nativa totalmente personalizable aquí
Mojtaba Hosseini

Respuestas:

211

A partir de Xcode 12 beta ( iOS 14 ), una nueva vista llamada ProgressViewestá disponible para los desarrolladores , y que puede mostrar un progreso tanto determinado como indeterminado.

Su estilo predeterminado es CircularProgressViewStyle, que es exactamente lo que estamos buscando.

var body: some View {
    VStack {
        ProgressView()
           // and if you want to be explicit / future-proof...
           // .progressViewStyle(CircularProgressViewStyle())
    }
}

Xcode 11.x

Algunas vistas aún no están representadas SwiftUI, pero es fácil trasladarlas al sistema. Necesitas envolver UIActivityIndicatory hacerlo UIViewRepresentable.

(Puede encontrar más información sobre esto en la excelente charla de la WWDC 2019: Integración de SwiftUI )

struct ActivityIndicator: UIViewRepresentable {

    @Binding var isAnimating: Bool
    let style: UIActivityIndicatorView.Style

    func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
        return UIActivityIndicatorView(style: style)
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
    }
}

Luego, puede usarlo de la siguiente manera: aquí hay un ejemplo de una superposición de carga.

Nota: prefiero usar ZStack, en lugar de overlay(:_), para saber exactamente lo que está sucediendo en mi implementación.

struct LoadingView<Content>: View where Content: View {

    @Binding var isShowing: Bool
    var content: () -> Content

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {

                self.content()
                    .disabled(self.isShowing)
                    .blur(radius: self.isShowing ? 3 : 0)

                VStack {
                    Text("Loading...")
                    ActivityIndicator(isAnimating: .constant(true), style: .large)
                }
                .frame(width: geometry.size.width / 2,
                       height: geometry.size.height / 5)
                .background(Color.secondary.colorInvert())
                .foregroundColor(Color.primary)
                .cornerRadius(20)
                .opacity(self.isShowing ? 1 : 0)

            }
        }
    }

}

Para probarlo, puede usar este código de ejemplo:

struct ContentView: View {

    var body: some View {
        LoadingView(isShowing: .constant(true)) {
            NavigationView {
                List(["1", "2", "3", "4", "5"], id: \.self) { row in
                    Text(row)
                }.navigationBarTitle(Text("A List"), displayMode: .large)
            }
        }
    }

}

Resultado:

ingrese la descripción de la imagen aquí

Matteo Pacini
fuente
1
Pero, ¿cómo detenerlo?
Bagusflyer
1
Hola @MatteoPacini, Gracias por tu respuesta. Pero, ¿podría ayudarme a ocultar el indicador de actividad? ¿Puede escribir el código para esto?
Annu
4
@Alfi en su código dice isShowing: .constant(true). Eso significa que el indicador siempre se muestra. Lo que debe hacer es tener una @Statevariable que sea verdadera cuando desee que aparezca el indicador de carga (cuando los datos se estén cargando) y luego cambiarla a falsa cuando desee que el indicador de carga desaparezca (cuando los datos terminen de cargarse) . Si se llama a la variable, isDataLoadingpor ejemplo, lo haría en isShowing: $isDataLoadinglugar de donde Matteo puso isShowing: .constant(true).
RPatel99
1
@MatteoPacini, en realidad no necesita un enlace para esto, ya que no se está modificando dentro de ActivityIndicator o en LoadingView. Solo una variable booleana normal funciona. La vinculación es útil para cuando desea modificar la variable dentro de la vista y devolver ese cambio al padre.
Helam
1
@nelsonPARRILLA Sospecho que tintColorsolo funciona en vistas de IU Swift puras, no en vistas puenteadas ( UIViewRepresentable).
Matteo Pacini
49

Si desea una solución de estilo de interfaz de usuario rápida , entonces esta es la magia:

import SwiftUI

struct ActivityIndicator: View {

  @State private var isAnimating: Bool = false

  var body: some View {
    GeometryReader { (geometry: GeometryProxy) in
      ForEach(0..<5) { index in
        Group {
          Circle()
            .frame(width: geometry.size.width / 5, height: geometry.size.height / 5)
            .scaleEffect(!self.isAnimating ? 1 - CGFloat(index) / 5 : 0.2 + CGFloat(index) / 5)
            .offset(y: geometry.size.width / 10 - geometry.size.height / 2)
          }.frame(width: geometry.size.width, height: geometry.size.height)
            .rotationEffect(!self.isAnimating ? .degrees(0) : .degrees(360))
            .animation(Animation
              .timingCurve(0.5, 0.15 + Double(index) / 5, 0.25, 1, duration: 1.5)
              .repeatForever(autoreverses: false))
        }
      }
    .aspectRatio(1, contentMode: .fit)
    .onAppear {
        self.isAnimating = true
    }
  }
}

Simplemente para usar:

ActivityIndicator()
.frame(width: 50, height: 50)

¡Espero eso ayude!

Ejemplo de uso:

ActivityIndicator()
.frame(size: CGSize(width: 200, height: 200))
    .foregroundColor(.orange)

ingrese la descripción de la imagen aquí

KitKit
fuente
Esto me ayudó mucho, ¡muchas gracias! Puede definir funciones para crear los Círculos y agregar un modificador de vista para las animaciones para que sean más legibles.
Arif Ata Cengiz
2
¡Amo esta solución!
Jon Vogel
1
¿Cómo eliminaría la animación si isAnimating es un estado, puede un @Binding en su lugar?
Di Nerd
40

iOS 14: nativo

es solo una vista simple.

ProgressView()

Actualmente, está predeterminado en CircularProgressViewStyle pero puede configurar manualmente el estilo agregando el siguiente modificador:

.progressViewStyle(CircularProgressViewStyle())

Además, el estilo puede ser cualquier cosa que se ajuste a ProgressViewStyle


iOS 13 - Estándar totalmente personalizable UIActivityIndicator en SwiftUI: (Exactamente como nativo View):

Puedes construirlo y configurarlo (tanto como puedas en el original UIKit):

ActivityIndicator(isAnimating: loading)
    .configure { $0.color = .yellow } // Optional configurations (🎁 bones)
    .background(Color.blue)

Resultado


Simplemente implemente esta base structy estará listo para comenzar:

struct ActivityIndicator: UIViewRepresentable {
    
    typealias UIView = UIActivityIndicatorView
    var isAnimating: Bool
    fileprivate var configuration = { (indicator: UIView) in }

    func makeUIView(context: UIViewRepresentableContext<Self>) -> UIView { UIView() }
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<Self>) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
        configuration(uiView)
    }
}

🎁 Extensión de huesos:

Con esta pequeña extensión útil, puede acceder a la configuración a través de un modifierSwiftUI como otros view:

extension View where Self == ActivityIndicator {
    func configure(_ configuration: @escaping (Self.UIView)->Void) -> Self {
        Self.init(isAnimating: self.isAnimating, configuration: configuration)
    }
}

La forma clásica:

También puede configurar la vista en un inicializador clásico:

ActivityIndicator(isAnimating: loading) { 
    $0.color = .red
    $0.hidesWhenStopped = false
    //Any other UIActivityIndicatorView property you like
}

Este método es totalmente adaptable. Por ejemplo, puede ver Cómo hacer que TextField se convierta en el primer respondedor con el mismo método aquí

Mojtaba Hosseini
fuente
¿Cómo cambiar el color de ProgressView?
Bagusflyer
.progressViewStyle(CircularProgressViewStyle(tint: Color.red))cambiará el color
Bagusflyer
5

Indicadores personalizados

Aunque Apple ahora es compatible con el indicador de actividad nativo de SwiftUI 2.0, simplemente puede implementar sus propias animaciones. Todos estos son compatibles con SwiftUI 1.0. También está trabajando en widgets.

Arcos

struct Arcs: View {
    @Binding var isAnimating: Bool
    let count: UInt
    let width: CGFloat
    let spacing: CGFloat

    var body: some View {
        GeometryReader { geometry in
            ForEach(0..<Int(count)) { index in
                item(forIndex: index, in: geometry.size)
                    .rotationEffect(isAnimating ? .degrees(360) : .degrees(0))
                    .animation(
                        Animation.default
                            .speed(Double.random(in: 0.2...0.5))
                            .repeatCount(isAnimating ? .max : 1, autoreverses: false)
                    )
            }
        }
        .aspectRatio(contentMode: .fit)
    }

    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
        Group { () -> Path in
            var p = Path()
            p.addArc(center: CGPoint(x: geometrySize.width/2, y: geometrySize.height/2),
                     radius: geometrySize.width/2 - width/2 - CGFloat(index) * (width + spacing),
                     startAngle: .degrees(0),
                     endAngle: .degrees(Double(Int.random(in: 120...300))),
                     clockwise: true)
            return p.strokedPath(.init(lineWidth: width))
        }
        .frame(width: geometrySize.width, height: geometrySize.height)
    }
}

Demo de diferentes variaciones Arcos


Barras

struct Bars: View {
    @Binding var isAnimating: Bool
    let count: UInt
    let spacing: CGFloat
    let cornerRadius: CGFloat
    let scaleRange: ClosedRange<Double>
    let opacityRange: ClosedRange<Double>

    var body: some View {
        GeometryReader { geometry in
            ForEach(0..<Int(count)) { index in
                item(forIndex: index, in: geometry.size)
            }
        }
        .aspectRatio(contentMode: .fit)
    }

    private var scale: CGFloat { CGFloat(isAnimating ? scaleRange.lowerBound : scaleRange.upperBound) }
    private var opacity: Double { isAnimating ? opacityRange.lowerBound : opacityRange.upperBound }

    private func size(count: UInt, geometry: CGSize) -> CGFloat {
        (geometry.width/CGFloat(count)) - (spacing-2)
    }

    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
        RoundedRectangle(cornerRadius: cornerRadius,  style: .continuous)
            .frame(width: size(count: count, geometry: geometrySize), height: geometrySize.height)
            .scaleEffect(x: 1, y: scale, anchor: .center)
            .opacity(opacity)
            .animation(
                Animation
                    .default
                    .repeatCount(isAnimating ? .max : 1, autoreverses: true)
                    .delay(Double(index) / Double(count) / 2)
            )
            .offset(x: CGFloat(index) * (size(count: count, geometry: geometrySize) + spacing))
    }
}

Demo de diferentes variaciones Barras


Intermitentes

struct Blinking: View {
    @Binding var isAnimating: Bool
    let count: UInt
    let size: CGFloat

    var body: some View {
        GeometryReader { geometry in
            ForEach(0..<Int(count)) { index in
                item(forIndex: index, in: geometry.size)
                    .frame(width: geometry.size.width, height: geometry.size.height)

            }
        }
        .aspectRatio(contentMode: .fit)
    }

    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
        let angle = 2 * CGFloat.pi / CGFloat(count) * CGFloat(index)
        let x = (geometrySize.width/2 - size/2) * cos(angle)
        let y = (geometrySize.height/2 - size/2) * sin(angle)
        return Circle()
            .frame(width: size, height: size)
            .scaleEffect(isAnimating ? 0.5 : 1)
            .opacity(isAnimating ? 0.25 : 1)
            .animation(
                Animation
                    .default
                    .repeatCount(isAnimating ? .max : 1, autoreverses: true)
                    .delay(Double(index) / Double(count) / 2)
            )
            .offset(x: x, y: y)
    }
}

Demo de diferentes variaciones Intermitentes


En aras de evitar paredes de código , puede encontrar indicadores más elegantes en este repositorio alojado en git .

Tenga en cuenta que todas estas animaciones tienen Bindingque DEBE alternar para ejecutarse.

Mojtaba Hosseini
fuente
¡Esto es genial! He encontrado un fallo, aunque - hay una muy extraña animación paraiActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222
¿Cuál es el problema con el iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))por cierto? @ pawello2222?
Mojtaba Hosseini
Si establece el counten 5 o menos, la animación se ve bien (se parece a esta respuesta ). Sin embargo, si establece el valor counten 15, el punto inicial no se detiene en la parte superior del círculo. Comienza a hacer otro ciclo, luego vuelve a la parte superior y luego comienza el ciclo nuevamente. No estoy seguro de si es intencionado. Probado solo en simulador, Xcode 12.0.1.
pawello2222
Hmmmm. Eso es porque las animaciones no se serializan. Debería agregar una opción de serialización al marco para eso. Gracias por compartir tu opinión.
Mojtaba Hosseini
@MojtabaHosseini ¿cómo se alterna el enlace para que se ejecute?
GarySabo
4

Implementé el indicador UIKit clásico usando SwiftUI. Vea el indicador de actividad en acción aquí

struct ActivityIndicator: View {
  @State private var currentIndex: Int = 0

  func incrementIndex() {
    currentIndex += 1
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50), execute: {
      self.incrementIndex()
    })
  }

  var body: some View {
    GeometryReader { (geometry: GeometryProxy) in
      ForEach(0..<12) { index in
        Group {
          Rectangle()
            .cornerRadius(geometry.size.width / 5)
            .frame(width: geometry.size.width / 8, height: geometry.size.height / 3)
            .offset(y: geometry.size.width / 2.25)
            .rotationEffect(.degrees(Double(-360 * index / 12)))
            .opacity(self.setOpacity(for: index))
        }.frame(width: geometry.size.width, height: geometry.size.height)
      }
    }
    .aspectRatio(1, contentMode: .fit)
    .onAppear {
      self.incrementIndex()
    }
  }

  func setOpacity(for index: Int) -> Double {
    let opacityOffset = Double((index + currentIndex - 1) % 11 ) / 12 * 0.9
    return 0.1 + opacityOffset
  }
}

struct ActivityIndicator_Previews: PreviewProvider {
  static var previews: some View {
    ActivityIndicator()
      .frame(width: 50, height: 50)
      .foregroundColor(.blue)
  }
}

Yisselda
fuente
3

Además de Mojatba Hosseini respuesta 's ,

Hice algunas actualizaciones para que esto se pueda poner en un paquete rápido :

Indicador de actividad:

import Foundation
import SwiftUI
import UIKit

public struct ActivityIndicator: UIViewRepresentable {

  public typealias UIView = UIActivityIndicatorView
  public var isAnimating: Bool = true
  public var configuration = { (indicator: UIView) in }

 public init(isAnimating: Bool, configuration: ((UIView) -> Void)? = nil) {
    self.isAnimating = isAnimating
    if let configuration = configuration {
        self.configuration = configuration
    }
 }

 public func makeUIView(context: UIViewRepresentableContext<Self>) -> UIView {
    UIView()
 }

 public func updateUIView(_ uiView: UIView, context: 
    UIViewRepresentableContext<Self>) {
     isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
     configuration(uiView)
}}

Extensión:

public extension View where Self == ActivityIndicator {
func configure(_ configuration: @escaping (Self.UIView) -> Void) -> Self {
    Self.init(isAnimating: self.isAnimating, configuration: configuration)
 }
}
moyoteg
fuente
2

Indicador de actividad en SwiftUI


import SwiftUI

struct Indicator: View {

    @State var animateTrimPath = false
    @State var rotaeInfinity = false

    var body: some View {

        ZStack {
            Color.black
                .edgesIgnoringSafeArea(.all)
            ZStack {
                Path { path in
                    path.addLines([
                        .init(x: 2, y: 1),
                        .init(x: 1, y: 0),
                        .init(x: 0, y: 1),
                        .init(x: 1, y: 2),
                        .init(x: 3, y: 0),
                        .init(x: 4, y: 1),
                        .init(x: 3, y: 2),
                        .init(x: 2, y: 1)
                    ])
                }
                .trim(from: animateTrimPath ? 1/0.99 : 0, to: animateTrimPath ? 1/0.99 : 1)
                .scale(50, anchor: .topLeading)
                .stroke(Color.yellow, lineWidth: 20)
                .offset(x: 110, y: 350)
                .animation(Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true))
                .onAppear() {
                    self.animateTrimPath.toggle()
                }
            }
            .rotationEffect(.degrees(rotaeInfinity ? 0 : -360))
            .scaleEffect(0.3, anchor: .center)
            .animation(Animation.easeInOut(duration: 1.5)
            .repeatForever(autoreverses: false))
            .onAppear(){
                self.rotaeInfinity.toggle()
            }
        }
    }
}

struct Indicator_Previews: PreviewProvider {
    static var previews: some View {
        Indicator()
    }
}

Indicador de actividad en SwiftUI

Rashid Latif
fuente
2

Prueba esto:

import SwiftUI

struct LoadingPlaceholder: View {
    var text = "Loading..."
    init(text:String ) {
        self.text = text
    }
    var body: some View {
        VStack(content: {
            ProgressView(self.text)
        })
    }
}

Más información sobre SwiftUI ProgressView

Pedro Trujillo
fuente
0
// Activity View

struct ActivityIndicator: UIViewRepresentable {

    let style: UIActivityIndicatorView.Style
    @Binding var animate: Bool

    private let spinner: UIActivityIndicatorView = {
        $0.hidesWhenStopped = true
        return $0
    }(UIActivityIndicatorView(style: .medium))

    func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
        spinner.style = style
        return spinner
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
        animate ? uiView.startAnimating() : uiView.stopAnimating()
    }

    func configure(_ indicator: (UIActivityIndicatorView) -> Void) -> some View {
        indicator(spinner)
        return self
    }   
}

// Usage
struct ContentView: View {

    @State var animate = false

    var body: some View {
            ActivityIndicator(style: .large, animate: $animate)
                .configure {
                    $0.color = .red
            }
            .background(Color.blue)
    }
}
Manish
fuente