Indicador de atividade em SwiftUI

97

Tentando adicionar um indicador de atividade em tela cheia no SwiftUI.

Posso usar a .overlay(overlay: )função no Viewprotocolo.

Com isso, posso fazer qualquer sobreposição de visualização, mas não consigo encontrar o estilo padrão do iOS UIActivityIndicatorViewequivalente em SwiftUI.

Como posso fazer um spinner de estilo padrão com SwiftUI?

NOTA: Não se trata de adicionar um indicador de atividade na estrutura UIKit.

Johnykutty
fonte
Também tentei encontrá-lo e não consegui, acho que será adicionado mais tarde :)
Markicevic
Certifique-se de registrar um problema de feedback com a Apple usando o Assistente de Feedback. Receber solicitações no início do processo beta é a melhor maneira de ver o que você deseja na estrutura.
Jon Shier
Você pode encontrar uma versão Padrão Nativa totalmente personalizável aqui
Mojtaba Hosseini

Respostas:

223

A partir do Xcode 12 beta ( iOS 14 ), uma nova visualização chamada ProgressViewé disponível para os desenvolvedores e pode exibir o progresso determinado e indeterminado.

Seu estilo padrão CircularProgressViewStyleé, que é exatamente o que estamos procurando.

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

Xcode 11.x

Algumas visualizações ainda não estão representadas SwiftUI, mas é fácil transportá-las para o sistema. Você precisa embrulhar UIActivityIndicatore fazer UIViewRepresentable.

(Mais sobre isso pode ser encontrado na excelente palestra WWDC 2019 - Integrando 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()
    }
}

Em seguida, você pode usá-lo da seguinte maneira - aqui está um exemplo de uma sobreposição de carregamento.

Observação: prefiro usar ZStack, em vez de overlay(:_), portanto, sei exatamente o que está acontecendo na minha implementação.

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 testá-lo, você pode usar este código de exemplo:

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:

insira a descrição da imagem aqui

Matteo Pacini
fonte
1
Mas como impedir?
Bagusflyer
1
Olá @MatteoPacini, Obrigado pela sua resposta. Mas, você pode me ajudar como posso ocultar o indicador de atividade. Você pode colocar o código para isso?
Annu de
4
@Alfi em seu código diz isShowing: .constant(true). Isso significa que o indicador está sempre sendo exibido. O que você precisa fazer é ter uma @Statevariável que seja verdadeira quando você quiser que o indicador de carregamento apareça (quando os dados estão carregando) e, em seguida, altere-a para falsa quando quiser que o indicador de carregamento desapareça (quando os dados terminarem de carregar) . Se a variável for chamada, isDataLoadingpor exemplo, você faria em isShowing: $isDataLoadingvez de onde Matteo colocou isShowing: .constant(true).
RPatel99 de
1
@MatteoPacini você realmente não precisa de um Binding para isso, pois ele não está sendo modificado dentro de ActivityIndicator ou em LoadingView. Apenas uma variável booleana regular funciona. A vinculação é útil quando você deseja modificar a variável dentro da visualização e passar essa alteração de volta para o pai.
Helam
1
@nelsonPARRILLA Suspeito que tintColorsó funcione em visualizações de IU Swift puras - não em bridge ( UIViewRepresentable).
Matteo Pacini
51

Se você deseja uma solução no estilo swift-ui , esta é a mágica:

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

Simplesmente para usar:

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

Espero que ajude!

Exemplo de uso:

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

insira a descrição da imagem aqui

KitKit
fonte
Isso me ajudou muito, muito obrigado! Você pode definir funções para criar os Círculos e adicionar modificador de visualização para as animações para torná-las mais legíveis.
Arif Ata Cengiz
2
Amei essa solução!
Jon Vogel
1
como eu removeria a animação se isAnimating fosse um estado, talvez um @Binding?
Di Nerd
44

iOS 14 - nativo

é apenas uma visão simples.

ProgressView()

Atualmente, o padrão é, CircularProgressViewStylemas você pode definir manualmente o estilo adicionando o seguinte modificador:

.progressViewStyle(CircularProgressViewStyle())

Além disso, o estilo pode ser qualquer coisa que esteja em conformidade com ProgressViewStyle


iOS 13 - Padrão totalmente personalizável UIActivityIndicatorem SwiftUI: (Exatamente como um nativo View):

Você pode construir e configurá-lo (tanto quanto você poderia no original UIKit):

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

Resultado


Basta implementar esta base structe você estará pronto para prosseguir:

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

🎁 Extensão de ossos:

Com esta pequena extensão útil, você pode acessar a configuração por meio de um modifieroutro SwiftUI views:

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

A maneira clássica:

Além disso, você pode configurar a visualização em um inicializador clássico:

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

Este método é totalmente adaptável. Por exemplo, você pode ver Como fazer TextField se tornar o primeiro a responder com o mesmo método aqui

Mojtaba Hosseini
fonte
Como mudar a cor do ProgressView?
Bagusflyer
.progressViewStyle(CircularProgressViewStyle(tint: Color.red))mudará a cor
Bagusflyer
Sua "Extensão bônus: configure ()" chama o init pela segunda vez, ocupando memória. Estou certo? ou é tão otimizado que podemos fazer tal invocação em cadeia do init?
Ruzard
É um açúcar, não é muito caro para este caso, mas não medi o impacto de desempenho para visualizações grandes. Você pode medir e alterar a implementação para algo mais eficiente (já que é uma classe), mas inicializar uma estrutura não é muito caro para se preocupar
Mojtaba Hosseini
8

Indicadores Customizados

Embora a Apple suporte o indicador de atividade nativo agora do SwiftUI 2.0, você pode simplesmente implementar suas próprias animações. Todos eles são suportados no SwiftUI 1.0. Também é funcionando em 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)
    }
}

Demonstração de diferentes variações 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))
    }
}

Demonstração de diferentes variações Barras


Bloqueios

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

Demonstração de diferentes variações Bloqueios


Para evitar paredes de código , você pode encontrar indicadores mais elegantes neste repo hospedado no git .

Observe que todas essas animações têm um Bindingque DEVE alternar para serem executadas.

Mojtaba Hosseini
fonte
Isso é ótimo! Eu encontrei um bug - há uma animação realmente estranha paraiActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222
qual é o problema com iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))a propósito? @ pawello2222?
Mojtaba Hosseini
Se você definir o countpara 5 ou menos, a animação parece boa (é semelhante a esta resposta ). No entanto, se você definir o countpara 15, o ponto inicial não pára no topo do círculo. Ele começa a fazer outro ciclo, depois volta ao topo e então começa o ciclo novamente. Não tenho certeza se foi intencional. Testado apenas no simulador, Xcode 12.0.1.
pawello2222
Hmmmm. Isso ocorre porque as animações não são serializadas. Devo adicionar uma opção de serialização para a estrutura para isso. Obrigado por compartilhar sua opinião.
Mojtaba Hosseini
@MojtabaHosseini como você alterna a ligação para executar?
GarySabo,
4

Implementei o indicador clássico UIKit usando SwiftUI. Veja o indicador de atividade em ação aqui

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
fonte
3

Além Mojatba Hosseini resposta 's ,

Fiz algumas atualizações para que isso possa ser colocado em um pacote rápido :

Indicador de atividade:

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

Extensão:

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

Indicador de atividade em 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 atividade em SwiftUI

Rashid Latif
fonte
2

Experimente isto:

import SwiftUI

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

Mais informações sobre em SwiftUI ProgressView

Pedro Trujillo
fonte
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
fonte
0

2021 - SwiftUI 2.0

ProgressView()
    .progressViewStyle(CircularProgressViewStyle())

insira a descrição da imagem aqui

Andrew
fonte
0
struct ContentView: View {
    
    @State private var isCircleRotating = true
    @State private var animateStart = false
    @State private var animateEnd = true
    
    var body: some View {
        
        ZStack {
            Circle()
                .stroke(lineWidth: 10)
                .fill(Color.init(red: 0.96, green: 0.96, blue: 0.96))
                .frame(width: 150, height: 150)
            
            Circle()
                .trim(from: animateStart ? 1/3 : 1/9, to: animateEnd ? 2/5 : 1)
                .stroke(lineWidth: 10)
                .rotationEffect(.degrees(isCircleRotating ? 360 : 0))
                .frame(width: 150, height: 150)
                .foregroundColor(Color.blue)
                .onAppear() {
                    withAnimation(Animation
                                    .linear(duration: 1)
                                    .repeatForever(autoreverses: false)) {
                        self.isCircleRotating.toggle()
                    }
                    withAnimation(Animation
                                    .linear(duration: 1)
                                    .delay(0.5)
                                    .repeatForever(autoreverses: true)) {
                        self.animateStart.toggle()
                    }
                    withAnimation(Animation
                                    .linear(duration: 1)
                                    .delay(1)
                                    .repeatForever(autoreverses: true)) {
                        self.animateEnd.toggle()
                    }
                }
        }
    }
}

insira a descrição da imagem aqui

Arvind Patel
fonte
0

É realmente fácil com o SwiftUI 2.0. Criei essa visualização personalizada simples e fácil com ProgressView

É assim que parece: Aqui

Código:

import SwiftUI

struct ActivityIndicatorView: View {
    @Binding var isPresented:Bool
    var body: some View {
        if isPresented{
            ZStack{
                RoundedRectangle(cornerRadius: 15).fill(CustomColor.gray.opacity(0.1))
                ProgressView {
                    Text("Loading...")
                        .font(.title2)
                }
            }.frame(width: 120, height: 120, alignment: .center)
            .background(RoundedRectangle(cornerRadius: 25).stroke(CustomColor.gray,lineWidth: 2))
        }
    }
}
batuhankrbb
fonte