Indikator aktivitas di SwiftUI

91

Mencoba menambahkan indikator aktivitas layar penuh di SwiftUI.

Saya dapat menggunakan .overlay(overlay: )fungsi dalam ViewProtokol.

Dengan ini, saya dapat membuat hamparan tampilan apa pun, tetapi saya tidak dapat menemukan gaya default iOS yang UIActivityIndicatorViewsetara SwiftUI.

Bagaimana cara membuat pemintal gaya default SwiftUI?

CATATAN: Ini bukan tentang menambahkan indikator aktivitas dalam kerangka UIKit.

Johnykutty
sumber
Saya mencoba menemukannya juga, dan gagal, tebak itu akan ditambahkan nanti :)
Markicevic
Pastikan untuk mengajukan masalah umpan balik dengan Apple menggunakan Bantuan Umpan Balik. Mendapatkan permintaan di awal selama proses beta adalah cara terbaik untuk melihat apa yang Anda inginkan dalam framework.
Jon Shier
Anda dapat menemukan versi Standar Asli yang dapat disesuaikan sepenuhnya di sini
Mojtaba Hosseini

Jawaban:

213

Pada Xcode 12 beta ( iOS 14 ), pandangan baru yang disebut ProgressViewadalah tersedia untuk pengembang , dan yang dapat menampilkan baik determinate dan kemajuan tak tentu.

Gayanya ditetapkan secara default CircularProgressViewStyle, persis seperti yang kita cari.

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

Xcode 11.x

Cukup banyak tampilan yang belum terwakili SwiftUI, tetapi mudah untuk memasukkannya ke dalam sistem. Anda perlu membungkus UIActivityIndicatordan membuatnya UIViewRepresentable.

(Lebih lanjut tentang ini dapat ditemukan dalam pembicaraan WWDC 2019 yang luar biasa - Mengintegrasikan 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()
    }
}

Kemudian Anda dapat menggunakannya sebagai berikut - berikut adalah contoh overlay pemuatan.

Catatan: Saya lebih suka menggunakan ZStack, daripada overlay(:_), jadi saya tahu persis apa yang terjadi dalam implementasi saya.

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)

            }
        }
    }

}

Untuk mengujinya, Anda dapat menggunakan kode contoh ini:

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

}

Hasil:

masukkan deskripsi gambar di sini

Matteo Pacini
sumber
1
Tapi bagaimana cara menghentikannya?
Bagusflyer
1
Hai @MatteoPacini, Terima kasih atas jawaban Anda. Tapi, bisakah Anda membantu saya bagaimana saya bisa menyembunyikan indikator aktivitas. Bisakah Anda menuliskan kode untuk ini?
Annu
4
@Alfi dalam kodenya tertulis isShowing: .constant(true). Artinya, indikator selalu muncul. Yang perlu Anda lakukan adalah memiliki @Statevariabel yang benar ketika Anda ingin indikator pemuatan muncul (ketika data sedang dimuat), dan kemudian mengubahnya menjadi salah ketika Anda ingin indikator pemuatan menghilang (ketika data selesai dimuat) . Jika variabel dipanggil isDataLoadingmisalnya, Anda akan melakukan isShowing: $isDataLoadingalih - alih tempat Matteo meletakkan isShowing: .constant(true).
RPatel99
1
@MatteoPacini Anda sebenarnya tidak memerlukan Binding untuk ini karena tidak sedang diubah di dalam ActivityIndicator atau di LoadingView. Hanya variabel boolean biasa yang berfungsi. Binding berguna saat Anda ingin mengubah variabel di dalam tampilan dan meneruskan perubahan itu kembali ke induknya.
Helam
1
@nelsonPARRILLA Saya menduga bahwa tintColorhanya berfungsi pada tampilan Swift UI murni - bukan pada yang dijembatani ( UIViewRepresentable).
Matteo Pacini
49

Jika Anda menginginkan solusi gaya ui-cepat , inilah keajaibannya:

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

Cukup untuk digunakan:

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

Semoga membantu!

Contoh Penggunaan:

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

masukkan deskripsi gambar di sini

KitKit
sumber
Ini sangat membantu saya, terima kasih banyak! Anda dapat menentukan fungsi untuk membuat Lingkaran dan menambahkan pengubah tampilan untuk animasi agar lebih mudah dibaca.
Arif Ata Cengiz
2
Suka solusi ini!
Jon Vogel
1
bagaimana cara menghapus animasi jika isAnimating adalah sebuah State, bolehkah @Binding sebagai gantinya?
Di Nerd
40

iOS 14 - Asli

itu hanya tampilan sederhana.

ProgressView()

Saat ini, defaultnya CircularProgressViewStyletetapi Anda dapat mengatur gayanya secara manual dengan menambahkan modifer berikut:

.progressViewStyle(CircularProgressViewStyle())

Juga, gayanya bisa apa saja yang sesuai ProgressViewStyle


iOS 13 - Standar yang dapat disesuaikan sepenuhnya UIActivityIndicatordi SwiftUI: (Persis seperti aslinya View):

Anda dapat membangun dan mengkonfigurasinya (sebanyak yang Anda bisa dalam bahasa aslinya UIKit):

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

Hasil


Cukup terapkan basis ini structdan Anda akan baik-baik saja:

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

🎁 Ekstensi Tulang:

Dengan ekstensi kecil yang bermanfaat ini, Anda dapat mengakses konfigurasi melalui modifierSwiftUI seperti lainnya view:

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

Cara klasik:

Anda juga dapat mengonfigurasi tampilan di penginisialisasi klasik:

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

Metode ini dapat disesuaikan sepenuhnya. Misalnya, Anda dapat melihat Cara membuat TextField menjadi responden pertama dengan metode yang sama di sini

Mojtaba Hosseini
sumber
Bagaimana cara mengubah warna ProgressView?
Bagusflyer
.progressViewStyle(CircularProgressViewStyle(tint: Color.red))akan mengubah warna
Bagusflyer
5

Indikator Kustom

Meskipun Apple mendukung Indikator Aktivitas asli sekarang dari SwiftUI 2.0, Anda cukup mengimplementasikan animasi Anda sendiri. Ini semua didukung di SwiftUI 1.0. Juga yang bekerja di widget.

Busur

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 berbagai variasi Busur


Bar

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 berbagai variasi Bar


Penutup mata

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 berbagai variasi Penutup mata


Demi mencegah dinding kode , Anda dapat menemukan indikator yang lebih elegan di repo ini yang dihosting di git .

Perhatikan bahwa semua animasi ini memiliki tombolBinding yang HARUS dijalankan.

Mojtaba Hosseini
sumber
Ini bagus! Saya telah menemukan satu bug - ada animasi yang sangat aneh untukiActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222
apa masalahnya dengan the iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))by the way? @ purnama04?
Mojtaba Hosseini
Jika Anda menyetel countke 5 atau kurang, animasi terlihat bagus (terlihat mirip dengan jawaban ini ). Namun, jika Anda menyetel countke 15, titik awal tidak berhenti di atas lingkaran. Ini mulai melakukan siklus lain, kemudian kembali ke atas dan kemudian memulai siklus lagi. Saya tidak yakin apakah itu dimaksudkan. Diuji hanya di simulator, Xcode 12.0.1.
pawello2222
Hmmmm. Itu karena animasi tidak berseri. Saya harus menambahkan opsi serialisasi ke kerangka untuk itu. Terima kasih telah membagikan pendapat Anda.
Mojtaba Hosseini
@MojtabaHosseini bagaimana Anda mengaktifkan pengikatan untuk menjalankan?
GarySabo
4

Saya menerapkan indikator UIKit klasik menggunakan SwiftUI. Lihat indikator aktivitas beraksi di sini

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

Selain Mojatba Hosseini 's jawaban ,

Saya telah membuat beberapa pembaruan sehingga ini dapat dimasukkan ke dalam paket yang cepat :

Indikator aktivitas:

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

Perpanjangan:

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

Indikator aktivitas di 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()
    }
}

Indikator aktivitas di SwiftUI

Rashid Latif
sumber
2

Coba ini:

import SwiftUI

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

Informasi lebih lanjut tentang di SwiftUI ProgressView

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