Wskaźnik aktywności w SwiftUI


89

Próbuję dodać pełnoekranowy wskaźnik aktywności w SwiftUI.

Potrafię używać .overlay(overlay: )funkcji w Viewprotokole.

Dzięki temu mogę utworzyć dowolną nakładkę widoku, ale nie mogę znaleźć odpowiednika domyślnego stylu iOS UIActivityIndicatorVieww SwiftUI.

Jak mogę utworzyć domyślny spinner w stylu SwiftUI?

UWAGA: Nie chodzi o dodanie wskaźnika aktywności w frameworku UIKit.


Próbowałem go również znaleźć, ale nie udało mi się, zgaduję, że zostanie dodany później :)
Markicevic

Pamiętaj, aby zgłosić problem z opinią do Apple za pomocą Asystenta opinii. Otrzymywanie żądań na wczesnym etapie procesu beta to najlepszy sposób, aby zobaczyć, czego chcesz w ramach.
Jon Shier

Odpowiedzi:


211

Od wersji beta Xcode 12 ( iOS 14 ) nowy widok wywoływany ProgressViewjest dostępny dla programistów , który może wyświetlać zarówno określony, jak i nieokreślony postęp.

Jego domyślny styl CircularProgressViewStyleto dokładnie to, czego szukamy.

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

Xcode 11.x

Sporo widoków nie jest jeszcze reprezentowanych w programie SwiftUI, ale łatwo jest je przenieść do systemu. Musisz to owinąć UIActivityIndicatori zrobić UIViewRepresentable.

(Więcej na ten temat można znaleźć w doskonałym wykładzie 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()
    }
}

Następnie możesz go użyć w następujący sposób - oto przykład nakładki ładowania.

Uwaga: wolę używać ZStackniż overlay(:_), więc dokładnie wiem, co się dzieje w mojej implementacji.

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)

            }
        }
    }

}

Aby to przetestować, możesz użyć tego przykładowego kodu:

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

}

Wynik:

wprowadź opis obrazu tutaj


1
Ale jak to zatrzymać?
Bagusflyer

1
Cześć @MatteoPacini, Dziękuję za odpowiedź. Ale czy możesz mi pomóc, jak mogę ukryć wskaźnik aktywności. Czy możesz podać kod?
Annu

4
@Alfi w swoim kodzie mówi isShowing: .constant(true). Oznacza to, że wskaźnik jest zawsze wyświetlany. To, co musisz zrobić, to mieć @Statezmienną, która jest prawdziwa, gdy chcesz, aby pojawił się wskaźnik ładowania (podczas ładowania danych), a następnie zmień to na fałsz, gdy chcesz, aby wskaźnik ładowania zniknął (po zakończeniu ładowania danych) . Jeśli na przykład zmienna jest wywoływana isDataLoading, zrobiłbyś isShowing: $isDataLoadingzamiast tego, gdzie wstawił Matteo isShowing: .constant(true).
RPatel99

1
@MatteoPacini nie potrzebujesz do tego powiązania, ponieważ nie jest modyfikowany wewnątrz ActivityIndicator ani w LoadingView. Działa tylko zwykła zmienna boolowska. Powiązanie jest przydatne, gdy chcesz zmodyfikować zmienną w widoku i przekazać tę zmianę z powrotem do elementu nadrzędnego.
Helam

1
@nelsonPARRILLA Podejrzewam, że tintColordziała tylko w czystych widokach Swift UI - nie na widokach mostkowanych ( UIViewRepresentable).
Matteo Pacini

49

Jeśli chcesz uzyskać rozwiązanie w stylu szybkiego interfejsu użytkownika , oto 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
    }
  }
}

Po prostu w użyciu:

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

Mam nadzieję, że to pomoże!

Przykładowe zastosowanie:

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

wprowadź opis obrazu tutaj


To mi bardzo pomogło, wielkie dzięki! Możesz zdefiniować funkcje do tworzenia kręgów i dodać modyfikator widoku do animacji, aby uczynić je bardziej czytelnymi.
Arif Ata Cengiz

2
Uwielbiam to rozwiązanie!
Jon Vogel

1
w jaki sposób usunąć animację, jeśli isAnimating jest stanem, czy zamiast tego można użyć @Binding?
Di Nerd

40

iOS 14 - natywny

to tylko prosty widok.

ProgressView()

Obecnie jest to ustawienie domyślne, CircularProgressViewStyleale możesz ręcznie ustawić jego styl, dodając następujący modyfikator:

.progressViewStyle(CircularProgressViewStyle())

Styl może być również zgodny z ProgressViewStyle


iOS 13 - w pełni konfigurowalny standard UIActivityIndicatorw SwiftUI: (Dokładnie jak natywny View):

Możesz go zbudować i skonfigurować (tak bardzo, jak w oryginale UIKit):

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

Wynik


Po prostu zaimplementuj tę bazę structi będziesz gotowy:

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

🎁 Przedłużenie kości:

Dzięki temu niewielkiemu modifierpomocnemu rozszerzeniu możesz uzyskać dostęp do konfiguracji za pośrednictwem podobnych interfejsów SwiftUI view:

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

Klasyczny sposób:

Możesz również skonfigurować widok w klasycznym inicjatorze:

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

Ta metoda jest w pełni adaptowalna. Na przykład możesz zobaczyć, jak ustawić TextField jako pierwszą osobę odpowiadającą za pomocą tej samej metody tutaj


Jak zmienić kolor ProgressView?
Bagusflyer

.progressViewStyle(CircularProgressViewStyle(tint: Color.red))zmieni kolor
Bagusflyer

5

Wskaźniki niestandardowe

Chociaż Apple obsługuje teraz natywny wskaźnik aktywności z SwiftUI 2.0, możesz po prostu zaimplementować własne animacje. Wszystkie są obsługiwane w SwiftUI 1.0. Także to jest praca w widżetów.

Łuki

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 różnych odmian Łuki


Słupy

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 różnych odmian Słupy


Klapki na oczy

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 różnych odmian Klapki na oczy


W celu uniknięcia ścian kodu , w tym repozytorium hostowanym na gicie można znaleźć bardziej eleganckie wskaźniki .

Należy pamiętać, że wszystkie te animacje mają Bindingto KONIECZNIE przełącznik do uruchomienia.


To jest świetne! Znalazłem jednak jeden błąd - jest naprawdę dziwna animacja dlaiActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222

jaki jest problem z tym iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))przy okazji? @ pawello2222?
Mojtaba Hosseini

Jeśli ustawisz na count5 lub mniej, animacja wygląda dobrze (wygląda podobnie do tej odpowiedzi ). Jeśli jednak ustawisz na count15, wiodąca kropka nie zatrzyma się na górze okręgu. Zaczyna wykonywać kolejny cykl, po czym wraca na szczyt i ponownie rozpoczyna cykl. Nie jestem pewien, czy jest to zamierzone. Przetestowano tylko na symulatorze, Xcode 12.0.1.
pawello2222

Hmmmm. Dzieje się tak, ponieważ animacje nie są serializowane. W tym celu powinienem dodać opcję serializacji do frameworka. Dziękuję za podzielenie się swoją opinią.
Mojtaba Hosseini

@MojtabaHosseini jak przełączyć wiązanie na uruchomienie?
GarySabo

4

Zaimplementowałem klasyczny wskaźnik UIKit za pomocą SwiftUI. Zobacz wskaźnik aktywności w akcji tutaj

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

Oprócz Mojatba Hosseini „s odpowiedzi ,

Zrobiłem kilka aktualizacji, aby można je było szybko umieścić w pakiecie :

Wskaźnik aktywności:

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

Rozbudowa:

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

2

Wskaźnik aktywności w 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()
    }
}

Wskaźnik aktywności w SwiftUI


2

Spróbuj tego:

import SwiftUI

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

Więcej informacji na temat w 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)
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.