SwiftUI'de etkinlik göstergesi


97

SwiftUI'ye tam ekran etkinlik göstergesi eklemeye çalışıyorum.

Protokolde .overlay(overlay: )işlevi kullanabilirim View.

Bununla, herhangi bir görünüm bindirmesi yapabilirim, ancak iOS varsayılan stil UIActivityIndicatorVieweşdeğerini uygulamasında bulamıyorum SwiftUI.

İle varsayılan stil çarkı nasıl yapabilirim SwiftUI?

NOT: Bu, UIKit çerçevesinde aktivite göstergesi eklemekle ilgili değildir.


Ben de bulmaya çalıştım ve başarısız oldum, sanırım daha sonra eklenecek :)
Markicevic

Geri Bildirim Yardımcısı'nı kullanarak Apple ile bir geri bildirim sorunu bildirdiğinizden emin olun. Beta sürecinde istekleri erken almak, çerçevede ne istediğinizi görmenin en iyi yoludur.
Jon Shier

Yanıtlar:


223

İtibariyle Xcode 12 beta ( iOS 14 ), adlı yeni görünümü ProgressViewolan geliştiriciler tarafından kullanılabilir ve bu tanımlı ve tanımsız ilerlemesini hem görüntüleyebilir.

Stili varsayılan olarak CircularProgressViewStyle, tam da aradığımız şey bu.

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

Xcode 11.x

Oldukça az sayıda görünüm henüz içinde temsil edilmemiştir SwiftUI, ancak bunları sisteme taşımak kolaydır. Sarmanız UIActivityIndicatorve yapmanız gerekir UIViewRepresentable.

( Bununla ilgili daha fazla bilgi, mükemmel WWDC 2019 konuşması - SwiftUI Entegrasyonu'nda bulunabilir )

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

O zaman aşağıdaki gibi kullanabilirsiniz - işte bir yükleme kaplaması örneği.

Not: Ben kullanmak ZStackyerine kullanmayı tercih ederim overlay(:_), bu yüzden uygulamamda tam olarak neler olduğunu biliyorum.

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)

            }
        }
    }

}

Test etmek için şu örnek kodu kullanabilirsiniz:

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

}

Sonuç:

görüntü açıklamasını buraya girin


1
Ama nasıl durdurulur?
Bagusflyer

1
Merhaba @ MatteoPacini, Cevabınız için teşekkürler. Ancak, lütfen bana etkinlik göstergesini nasıl gizleyebilirim. Lütfen bunun kodunu yazabilir misin?
Annu

4
Kodunda @Alfi diyor isShowing: .constant(true). Bu, göstergenin her zaman gösterdiği anlamına gelir. Yapmanız gereken @State, yükleme göstergesinin görünmesini istediğinizde (veri yüklenirken) doğru olan bir değişkene sahip olmak ve ardından yükleme göstergesinin kaybolmasını istediğinizde (veri yükleme tamamlandığında) bunu yanlış olarak değiştirmektir . isDataLoadingÖrneğin değişken çağrılırsa , isShowing: $isDataLoadingMatteo'nun koyduğu yer yerine yaparsınız isShowing: .constant(true).
RPatel99

1
@MatteoPacini ActivityIndicator veya LoadingView içinde değiştirilmediği için aslında bunun için bir Bağlamaya ihtiyacınız yoktur. Sadece normal bir boolean değişkeni çalışır. Bağlama, görünüm içindeki değişkeni değiştirmek ve bu değişikliği üst öğeye geri vermek istediğinizde kullanışlıdır.
Helam

1
@nelsonPARRILLA tintColorSadece saf Swift UI görünümlerinde çalıştığından şüpheleniyorum - köprülü ( UIViewRepresentable) olanlarda değil.
Matteo Pacini

51

Hızlı UI tarzı bir çözüm istiyorsanız , sihir budur:

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

Sadece kullanmak için:

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

Umarım yardımcı olur!

Örnek Kullanım:

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

görüntü açıklamasını buraya girin


Bu bana çok yardımcı oldu, çok teşekkürler! Çevreler oluşturmak için işlevler tanımlayabilir ve animasyonları daha okunaklı hale getirmek için animasyonlar için görünüm değiştirici ekleyebilirsiniz.
Arif Ata Cengiz

2
Bu çözümü seviyorum!
Jon Vogel

1
isAnimating bir Durum ise animasyonu nasıl kaldırırım, bunun yerine @Binding olabilir mi?
Di Nerd

44

iOS 14 - Yerel

bu sadece basit bir görünüm.

ProgressView()

Şu anda varsayılan olarak CircularProgressViewStyle ayarlanmıştır, ancak aşağıdaki değiştiriciyi ekleyerek stilini manuel olarak ayarlayabilirsiniz:

.progressViewStyle(CircularProgressViewStyle())

Ayrıca stil, uyan herhangi bir şey olabilir. ProgressViewStyle


iOS 13 - Tamamen özelleştirilebilir Standart UIActivityIndicator SwiftUI'de : (Tam olarak yerel olarak View):

Oluşturabilir ve yapılandırabilirsiniz (orijinalde yapabildiğiniz kadar UIKit):

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

Sonuç


Sadece bu temeli uygulayın structve gitmekte fayda var:

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

🎁 Kemik Uzantısı:

Bu küçük yardımcı uzantı ile yapılandırmaya modifierdiğer SwiftUI'ler gibi erişebilirsiniz view:

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

Klasik yol:

Ayrıca görünümü klasik bir başlatıcıda yapılandırabilirsiniz:

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

Bu yöntem tamamen uyarlanabilir. Örneğin, TextField'ın aynı yöntemle nasıl ilk yanıt veren olmasını sağladığını burada görebilirsiniz.


ProgressView'in rengi nasıl değiştirilir?
Bagusflyer

.progressViewStyle(CircularProgressViewStyle(tint: Color.red))rengi değiştirecek
Bagusflyer

"Bonus Uzantınız: configure ()" çağrılarını ikinci kez başlatarak hafızayı işgal eder. Haklı mıyım yoksa o kadar yüksek düzeyde optimize edilmiş ki, böyle bir zincir başlatma çağrısı yapmamıza izin veriliyor mu?
Ruzard

Bu bir şeker, bu durum için çok pahalı değil ama büyük görüntülemeler için vurulan performansı ölçmedim. Uygulamayı daha verimli bir şeye ölçebilir ve değiştirebilirsiniz (bu bir sınıf olduğu için), ancak bir yapıyı başlatmak endişelenecek kadar pahalı değil
Mojtaba Hosseini

8

Özel Göstergeler

Apple artık SwiftUI 2.0'dan yerel Etkinlik Göstergesini desteklese de, Kendi animasyonlarınızı kolayca uygulayabilirsiniz. Bunların tümü SwiftUI 1.0'da desteklenmektedir. Ayrıca edilir widget çalışan.

Yaylar

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

Farklı varyasyonların demosu Yaylar


Barlar

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

Farklı varyasyonların demosu Barlar


Flaşörler

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

Farklı varyasyonların demosu Flaşörler


Kod duvarlarını önlemek adına, git üzerinde barındırılan bu depoda daha zarif göstergeler bulabilirsiniz .

Tüm bu animasyonların çalıştırılması için geçiş yapılması GEREKEN bir özelliğe sahip Bindingolduğuna dikkat edin .


Bu harika! Yine de bir hata buldum - için gerçekten garip bir animasyon variActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222

iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))bu arada sorun nedir? @ pawello2222?
Mojtaba Hosseini

count5 veya daha azını ayarlarsanız, animasyon iyi görünür ( bu yanıta benzer ). Ancak, count15 olarak ayarlarsanız, baştaki nokta dairenin üstünde durmaz . Daha sonra, başka bir döngü yapıyor başlar geliyor geri üstüne ve sonra yeniden çevrimini başlatır. Kasıtlı olup olmadığından emin değilim. Yalnızca simülatörde test edilmiştir, Xcode 12.0.1.
pawello2222

Hmmmm. Bunun nedeni, animasyonların serileştirilmemesidir. Bunun için çerçeveye bir serileştirme seçeneği eklemeliyim. Fikrinizi paylaştığınız için teşekkürler.
Mojtaba Hosseini

@MojtabaHosseini Bağlamayı çalıştırmak için nasıl değiştirirsiniz?
GarySabo

4

SwiftUI kullanarak klasik UIKit göstergesini uyguladım. Burada etkinlik göstergesinin nasıl çalıştığını görün

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

Ek olarak Mojatba Hosseini bireyin cevap ,

Bunun hızlı bir pakete konulabilmesi için birkaç güncelleme yaptım :

Aktivite göstergesi:

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

Uzantı:

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

2

SwiftUI'de etkinlik göstergesi


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

SwiftUI'de etkinlik göstergesi


2

Bunu dene:

import SwiftUI

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

SwiftUI ProgressView hakkında daha fazla bilgi


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

görüntü açıklamasını buraya girin


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

görüntü açıklamasını buraya girin


0

SwiftUI 2.0 ile gerçekten çok kolay Bu basit ve kolay özel görünümü ile ProgressView

İşte nasıl göründüğü: İşte

Kod:

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))
        }
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.