Aktivitätsanzeige in SwiftUI

91

Es wurde versucht, in SwiftUI eine Vollbild-Aktivitätsanzeige hinzuzufügen.

Ich kann die .overlay(overlay: )Funktion im ViewProtokoll verwenden.

Damit kann ich jede Ansicht Overlay machen, aber ich kann nicht die iOS Standardstil finden UIActivityIndicatorViewin gleichwertig SwiftUI.

Wie kann ich einen Standard-Style-Spinner erstellen SwiftUI?

HINWEIS: Hier geht es nicht um das Hinzufügen eines Aktivitätsindikators im UIKit-Framework.

Johnykutty
quelle
Ich habe versucht, es auch zu finden, und bin gescheitert, schätze, es wird später hinzugefügt :)
Markicevic
Stellen Sie sicher, dass Sie mit dem Feedback-Assistenten ein Feedback-Problem bei Apple einreichen. Das frühzeitige Einholen von Anfragen während des Beta-Prozesses ist der beste Weg, um zu sehen, was Sie im Framework wollen.
Jon Shier

Antworten:

213

Ab Xcode 12 Beta ( iOS 14 ) steht Entwicklern eine neue Ansicht mit dem Namen ProgressViewzur Verfügung, die sowohl einen bestimmten als auch einen unbestimmten Fortschritt anzeigen kann.

Der Stil ist standardmäßig CircularProgressViewStylegenau das, wonach wir suchen.

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

Xcode 11.x.

Nicht wenige Ansichten sind noch nicht dargestellt SwiftUI, aber es ist einfach, sie in das System zu portieren. Sie müssen wickeln UIActivityIndicatorund es machen UIViewRepresentable.

(Mehr dazu finden Sie im ausgezeichneten WWDC 2019-Vortrag - Integration von 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()
    }
}

Dann können Sie es wie folgt verwenden - hier ist ein Beispiel für eine Ladeüberlagerung.

Hinweis: Ich bevorzuge die Verwendung ZStack, anstatt overlay(:_)genau zu wissen, was in meiner Implementierung vor sich geht.

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)

            }
        }
    }

}

Zum Testen können Sie diesen Beispielcode verwenden:

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

}

Ergebnis:

Geben Sie hier die Bildbeschreibung ein

Matteo Pacini
quelle
1
Aber wie kann man das aufhalten?
Bagusflyer
1
Hallo @MatteoPacini, Danke für deine Antwort. Aber können Sie mir bitte helfen, wie ich den Aktivitätsindikator ausblenden kann? Können Sie bitte den Code dafür notieren?
Annu
4
@ Alfi in seinem Code steht isShowing: .constant(true). Das heißt, der Indikator wird immer angezeigt. Was Sie tun müssen, ist eine @StateVariable zu haben, die wahr ist, wenn der Ladeindikator angezeigt werden soll (wenn die Daten geladen werden), und diese dann in falsch zu ändern, wenn der Ladeindikator verschwinden soll (wenn die Daten geladen sind). . Wenn die Variable isDataLoadingzum Beispiel aufgerufen wird , würden Sie isShowing: $isDataLoadinganstelle von Matteo setzen isShowing: .constant(true).
RPatel99
1
@MatteoPacini Sie benötigen hierfür keine Bindung, da diese weder in ActivityIndicator noch in LoadingView geändert wird. Nur eine reguläre boolesche Variable funktioniert. Die Bindung ist nützlich, wenn Sie die Variable in der Ansicht ändern und diese Änderung wieder an das übergeordnete Element übergeben möchten.
Helam
1
@nelsonPARRILLA Ich vermute, dass dies tintColornur bei reinen Swift-UI-Ansichten funktioniert - nicht bei Bridged ( UIViewRepresentable).
Matteo Pacini
49

Wenn Sie eine Lösung im Swift-UI-Stil suchen, dann ist dies die Magie:

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

Einfach zu bedienen:

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

Ich hoffe es hilft!

Anwendungsbeispiel:

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

Geben Sie hier die Bildbeschreibung ein

KitKit
quelle
Das hat mir sehr geholfen, vielen Dank! Sie können Funktionen zum Erstellen der Kreise definieren und einen Ansichtsmodifikator für die Animationen hinzufügen, um sie besser lesbar zu machen.
Arif Ata Cengiz
2
Ich liebe diese Lösung!
Jon Vogel
1
Wie würde ich Animation entfernen, wenn isAnimating ein Status ist, kann stattdessen ein @Binding sein?
Di Nerd
40

iOS 14 - Native

Es ist nur eine einfache Ansicht.

ProgressView()

Derzeit ist dies CircularProgressViewStylestandardmäßig aktiviert. Sie können den Stil jedoch manuell festlegen, indem Sie den folgenden Modifikator hinzufügen:

.progressViewStyle(CircularProgressViewStyle())

Der Stil könnte auch alles sein, was passt ProgressViewStyle


iOS 13 - Vollständig anpassbarer Standard UIActivityIndicatorin SwiftUI: (Genau als native View):

Sie können es erstellen und konfigurieren (so viel wie im Original UIKit):

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

Ergebnis


Implementieren structSie einfach diese Basis und Sie können loslegen:

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

🎁 Knochenverlängerung:

Mit dieser kleinen hilfreichen Erweiterung können Sie modifierwie bei anderen SwiftUIs auf die Konfiguration zugreifen view:

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

Der klassische Weg:

Sie können die Ansicht auch in einem klassischen Initialisierer konfigurieren:

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

Diese Methode ist vollständig anpassbar. Zum Beispiel können Sie sehen , wie TextField- werden die Ersthelfer machen mit dem gleichen Verfahren hier

Mojtaba Hosseini
quelle
Wie ändere ich die Farbe von ProgressView?
Bagusflyer
.progressViewStyle(CircularProgressViewStyle(tint: Color.red))wird die Farbe ändern
Bagusflyer
5

Benutzerdefinierte Indikatoren

Obwohl Apple jetzt den nativen Aktivitätsindikator von SwiftUI 2.0 unterstützt, können Sie einfach Ihre eigenen Animationen implementieren. Diese werden alle von SwiftUI 1.0 unterstützt. Es auch ist , arbeitet in Widgets.

Bögen

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 verschiedener Variationen Bögen


Riegel

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 verschiedener Variationen Riegel


Scheuklappen

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 verschiedener Variationen Scheuklappen


Aus Gründen der Verhinderung Wände des Codes , können Sie elegantere Indikatoren finden diese Repo auf dem git gehostet .

Beachten Sie, dass alle diese Animationen haben , Bindingdass MUST Toggle ausgeführt werden soll.

Mojtaba Hosseini
quelle
Das ist toll! Ich habe jedoch einen Fehler gefunden - es gibt eine wirklich seltsame Animation füriActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222
Was ist das Problem mit iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))dem übrigens? @ pawello2222?
Mojtaba Hosseini
Wenn Sie den Wert countauf 5 oder weniger einstellen , sieht die Animation gut aus (ähnelt dieser Antwort ). Wenn Sie jedoch den Wert countauf 15 setzen, stoppt der führende Punkt nicht am oberen Rand des Kreises. Es beginnt einen weiteren Zyklus, kehrt dann nach oben zurück und startet den Zyklus erneut. Ich bin mir nicht sicher, ob es beabsichtigt ist. Nur auf dem Simulator getestet, Xcode 12.0.1.
pawello2222
Hmmmm. Das liegt daran, dass Animationen nicht serialisiert werden. Ich sollte dem Framework dafür eine Serialisierungsoption hinzufügen. Vielen Dank für Ihre Meinung.
Mojtaba Hosseini
@MojtabaHosseini Wie schaltest du die Bindung um, um sie auszuführen?
GarySabo
4

Ich habe den klassischen UIKit-Indikator mit SwiftUI implementiert. Sehen Sie hier den Aktivitätsindikator in Aktion

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

Zusätzlich zu Mojatba Hosseinis Antwort ,

Ich habe ein paar Updates vorgenommen, damit dies in ein schnelles Paket gepackt werden kann :

Aktivitätsindikator:

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

Erweiterung:

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

Aktivitätsanzeige 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()
    }
}

Aktivitätsanzeige in SwiftUI

Rashid Latif
quelle
2

Versuche dies:

import SwiftUI

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

Weitere Informationen zu SwiftUI ProgressView

Pedro Trujillo
quelle
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
quelle