Indicatore di attività in SwiftUI


97

Tentativo di aggiungere un indicatore di attività a schermo intero in SwiftUI.

Posso usare la .overlay(overlay: )funzione in ViewProtocol.

Con questo, posso creare qualsiasi sovrapposizione di visualizzazione, ma non riesco a trovare l' UIActivityIndicatorViewequivalente dello stile predefinito di iOS in SwiftUI.

Come posso creare uno stile di rotazione predefinito con SwiftUI?

NOTA: non si tratta di aggiungere l'indicatore di attività nel framework UIKit.


Ho provato a trovarlo anche, e ho fallito, immagino che verrà aggiunto più tardi :)
Markicevic

Assicurati di presentare un problema di feedback con Apple utilizzando l'Assistente feedback. Ricevere richieste nelle prime fasi del processo beta è il modo migliore per vedere ciò che desideri nel framework.
Jon Shier,

Risposte:


223

A partire da Xcode 12 beta ( iOS 14 ), una nuova vista chiamata ProgressViewè disponibile per gli sviluppatori e che può visualizzare progressi sia determinati che indeterminati.

Il suo stile è predefinito CircularProgressViewStyle, che è esattamente quello che stiamo cercando.

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

Xcode 11.x

Molte viste non sono ancora rappresentate in SwiftUI, ma è facile portarle nel sistema. Devi avvolgerlo UIActivityIndicatore farlo UIViewRepresentable.

(Maggiori informazioni su questo possono essere trovate nell'eccellente discorso del WWDC 2019 - Integrating 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()
    }
}

Quindi puoi usarlo come segue: ecco un esempio di sovrapposizione di caricamento.

Nota: preferisco usare ZStack, piuttosto che overlay(:_), quindi so esattamente cosa sta succedendo nella mia implementazione.

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)

            }
        }
    }

}

Per testarlo, puoi utilizzare questo codice di esempio:

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

}

Risultato:

inserisci qui la descrizione dell'immagine


1
Ma come fermarlo?
Bagusflyer

1
Ciao @MatteoPacini, grazie per la tua risposta. Ma puoi aiutarmi per favore come posso nascondere l'indicatore di attività. Puoi mettere giù il codice per questo?
Annu

4
@Alfi nel suo codice dice isShowing: .constant(true). Ciò significa che l'indicatore è sempre visualizzato. Quello che devi fare è avere una @Statevariabile che sia vera quando vuoi che appaia l'indicatore di caricamento (quando i dati si stanno caricando), e poi cambiarla in false quando vuoi che l'indicatore di caricamento scompaia (quando i dati hanno terminato il caricamento) . Se isDataLoadingad esempio viene chiamata la variabile , faresti isShowing: $isDataLoadinginvece di dove ha messo Matteo isShowing: .constant(true).
RPatel99

1
@MatteoPacini in realtà non è necessario un Binding per questo in quanto non viene modificato all'interno di ActivityIndicator o in LoadingView. Solo una normale variabile booleana funziona. Il binding è utile quando si desidera modificare la variabile all'interno della vista e restituire la modifica al genitore.
Helam

1
@nelsonPARRILLA Sospetto che funzioni tintColorsolo su viste dell'interfaccia utente Swift pure, non su quelle con bridge ( UIViewRepresentable).
Matteo Pacini

51

Se vuoi una soluzione rapida in stile ui , questa è 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
    }
  }
}

Semplicemente da usare:

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

Spero che sia d'aiuto!

Utilizzo di esempio:

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

inserisci qui la descrizione dell'immagine


Questo mi ha aiutato tantissimo, grazie mille! Puoi definire funzioni per creare i cerchi e aggiungere un modificatore di visualizzazione per le animazioni per renderlo più leggibile.
Arif Ata Cengiz

2
Adoro questa soluzione!
Jon Vogel

1
come rimuoverei l'animazione se isAnimating è uno stato, può invece un @Binding?
Di Nerd

44

iOS 14: nativo

è solo una semplice visualizzazione.

ProgressView()

Attualmente, è impostato per impostazione CircularProgressViewStylepredefinita, ma puoi impostarne manualmente lo stile aggiungendo il seguente modificatore:

.progressViewStyle(CircularProgressViewStyle())

Inoltre, lo stile potrebbe essere qualsiasi cosa conforme ProgressViewStyle


iOS 13 - Standard completamente personalizzabile UIActivityIndicatorin SwiftUI: (esattamente come nativo View):

Puoi costruirlo e configurarlo (tanto quanto potresti nell'originale UIKit):

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

Risultato


Basta implementare questa base structe sarai a posto:

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

🎁 Estensione delle ossa:

Con questa piccola utile estensione, puoi accedere alla configurazione tramite uno modifiercome altri SwiftUI view:

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

Il modo classico:

Inoltre puoi configurare la vista in un inizializzatore classico:

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

Questo metodo è completamente adattabile. Ad esempio, puoi vedere come fare in modo che TextField diventi il ​​primo risponditore con lo stesso metodo qui


Come cambiare il colore di ProgressView?
Bagusflyer

.progressViewStyle(CircularProgressViewStyle(tint: Color.red))cambierà il colore
Bagusflyer

Il tuo "Bonus Extension: configure ()" chiama init per la seconda volta, occupando memoria. Ho ragione? o è così altamente ottimizzato che ci è permesso fare una simile chiamata a catena di init?
Ruzard

È uno zucchero, non è molto costoso per questo caso, ma non ho misurato il rendimento per grandi visualizzazioni. Puoi misurare e modificare l'implementazione in qualcosa di più efficiente (dato che è una classe) ma l'inizializzazione di una struttura non è molto costosa di cui preoccuparsi
Mojtaba Hosseini

8

Indicatori personalizzati

Sebbene Apple supporti l'indicatore di attività nativo ora da SwiftUI 2.0, puoi semplicemente implementare le tue animazioni. Questi sono tutti supportati su SwiftUI 1.0. Inoltre sta lavorando in widget.

Archi

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 di diverse varianti Archi


Barre

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 di diverse varianti Barre


Paraocchi

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 di diverse varianti Paraocchi


Per evitare muri di codice , puoi trovare indicatori più eleganti in questo repository ospitato su git .

Si noti che tutte queste animazioni hanno una Bindingche MUST ginocchiera da eseguire.


È fantastico! Ho trovato un bug però - c'è un'animazione davvero strana periActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222

qual è il problema con il iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))tra l'altro? @ pawello2222?
Mojtaba Hosseini

Se imposti il countsu 5 o meno, l'animazione sembra a posto (è simile a questa risposta ). Tuttavia, se si imposta countsu 15, il punto iniziale non si ferma nella parte superiore del cerchio. Inizia a fare un altro ciclo, quindi torna all'inizio e poi ricomincia il ciclo. Non sono sicuro che sia intenzionale. Testato solo sul simulatore, Xcode 12.0.1.
pawello2222

Hmmmm. Questo perché le animazioni non vengono serializzate. Dovrei aggiungere un'opzione di serializzazione al framework per questo. Grazie per aver condiviso la tua opinione.
Mojtaba Hosseini

@MojtabaHosseini come si attiva l'associazione per l'esecuzione?
GarySabo,

4

Ho implementato il classico indicatore UIKit utilizzando SwiftUI. Guarda l'indicatore di attività in azione qui

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


3

Oltre a Mojatba Hosseini 's risposta ,

Ho apportato alcuni aggiornamenti in modo che questo possa essere inserito in un pacchetto rapido :

Indicatore di attività:

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

Estensione:

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

2

Indicatore di attività in 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()
    }
}

Indicatore di attività in SwiftUI


2

Prova questo:

import SwiftUI

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

Maggiori informazioni su SwiftUI ProgressView


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

0

2021 - SwiftUI 2.0

ProgressView()
    .progressViewStyle(CircularProgressViewStyle())

inserisci qui la descrizione dell'immagine


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

inserisci qui la descrizione dell'immagine


0

È davvero facile con SwiftUI 2.0 con cui ho creato questa visualizzazione personalizzata semplice e facile ProgressView

Ecco come appare: qui

Codice:

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))
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.