Chỉ báo hoạt động trong SwiftUI


91

Đang cố gắng thêm chỉ báo hoạt động toàn màn hình trong SwiftUI.

Tôi có thể sử dụng .overlay(overlay: )chức năng trong ViewGiao thức.

Với điều này, tôi có thể tạo bất kỳ lớp phủ chế độ xem nào, nhưng tôi không thể tìm thấy kiểu mặc định iOS UIActivityIndicatorViewtương đương trong SwiftUI.

Làm cách nào để tạo một vòng quay kiểu mặc định SwiftUI?

LƯU Ý: Đây không phải là việc thêm chỉ báo hoạt động trong khuôn khổ UIKit.


Tôi cố gắng để tìm thấy nó cũng có, và thất bại, đoán nó sẽ được bổ sung sau :)
Markicevic

Đảm bảo gửi phản hồi vấn đề với Apple bằng Hỗ trợ phản hồi. Nhận yêu cầu sớm trong quá trình beta là cách tốt nhất để xem bạn muốn gì trong khuôn khổ.
Jon Shier

Câu trả lời:


213

Kể từ phiên bản Xcode 12 beta ( iOS 14 ), chế độ xem mới được gọi ProgressViewcó sẵn cho các nhà phát triển và có thể hiển thị cả tiến trình xác định và không xác định.

Phong cách của nó được mặc định CircularProgressViewStyle, đó chính xác là những gì chúng tôi đang tìm kiếm.

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

Xcode 11.x

Khá nhiều chế độ xem chưa được biểu diễn SwiftUI, nhưng có thể dễ dàng chuyển chúng vào hệ thống. Bạn cần phải bọc UIActivityIndicatorvà làm cho nó UIViewRepresentable.

(Có thể tìm hiểu thêm về điều này trong bài nói chuyện tuyệt vời về WWDC 2019 - Tích hợp 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()
    }
}

Sau đó, bạn có thể sử dụng nó như sau - đây là một ví dụ về lớp phủ tải.

Lưu ý: Tôi thích sử dụng ZStackhơn là sử dụng , vì overlay(:_)vậy tôi biết chính xác những gì đang xảy ra trong quá trình triển khai của mình.

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)

            }
        }
    }

}

Để kiểm tra nó, bạn có thể sử dụng mã ví dụ sau:

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

}

Kết quả:

nhập mô tả hình ảnh ở đây


1
Nhưng làm thế nào để ngăn chặn nó?
Bagusflyer

1
Xin chào @MatteoPacini, Cảm ơn câu trả lời của bạn. Nhưng, bạn có thể vui lòng giúp tôi cách ẩn chỉ báo hoạt động không. Bạn có thể vui lòng đặt mã cho cái này không?
Annu

4
@Alfi trong mã của anh ấy có ghi isShowing: .constant(true). Điều đó có nghĩa là chỉ báo luôn hiển thị. Những gì bạn cần làm là có một @Statebiến là true khi bạn muốn chỉ báo tải xuất hiện (khi dữ liệu đang tải), sau đó thay đổi biến đó thành false khi bạn muốn chỉ báo tải biến mất (khi tải xong dữ liệu) . isDataLoadingVí dụ, nếu biến được gọi , bạn sẽ làm isShowing: $isDataLoadingthay vì nơi Matteo đặt isShowing: .constant(true).
RPatel99

1
@MatteoPacini, bạn thực sự không cần Binding cho việc này vì nó không bị sửa đổi bên trong ActivityIndicator hoặc trong LoadingView. Chỉ một biến boolean thông thường hoạt động. Ràng buộc hữu ích khi bạn muốn sửa đổi biến bên trong dạng xem và chuyển lại thay đổi đó cho cha mẹ.
Helam ngày

1
@nelsonPARRILLA Tôi nghi ngờ rằng điều đó tintColorchỉ hoạt động trên các chế độ xem giao diện người dùng Swift thuần túy - không hoạt động trên các chế độ xem bắc cầu ( UIViewRepresentable).
Matteo Pacini

49

Nếu bạn muốn có một giải pháp theo phong cách nhanh chóng , thì đây là điều kỳ diệu:

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

Chỉ cần sử dụng:

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

Hy vọng nó giúp!

Cách sử dụng ví dụ:

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

nhập mô tả hình ảnh ở đây


Điều này đã giúp tôi rất nhiều, cảm ơn rất nhiều! Bạn có thể xác định các chức năng để tạo Vòng kết nối và thêm công cụ sửa đổi chế độ xem cho các hoạt ảnh để làm cho nó dễ đọc hơn.
Arif Ata Cengiz

2
Thích giải pháp này!
Jon Vogel

1
Làm cách nào để xóa hoạt ảnh nếu isAnimating là một Trạng thái, thay vào đó có thể là @Binding không?
Di Nerd

40

iOS 14 - Gốc

nó chỉ là một cái nhìn đơn giản.

ProgressView()

Hiện tại, nó được đặt mặc định thành CircularProgressViewStylenhưng bạn có thể đặt kiểu thủ công bằng cách thêm modifer sau:

.progressViewStyle(CircularProgressViewStyle())

Ngoài ra, phong cách có thể là bất kỳ thứ gì phù hợp với ProgressViewStyle


iOS 13 - Tiêu chuẩn hoàn toàn có thể tùy chỉnh UIActivityIndicatortrong SwiftUI: (Chính xác là bản gốc View):

Bạn có thể xây dựng và định cấu hình nó (càng nhiều càng tốt trong bản gốc UIKit):

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

Kết quả


Chỉ cần triển khai cơ sở này structvà bạn sẽ có thể đi:

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

🎁 Mở rộng xương:

Với tiện ích mở rộng hữu ích nhỏ này, bạn có thể truy cập cấu hình thông qua một modifierSwiftUI giống như các views:

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

Cách cổ điển:

Ngoài ra, bạn có thể định cấu hình chế độ xem trong trình khởi tạo cổ điển:

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

Phương pháp này hoàn toàn có thể thích ứng. Ví dụ: bạn có thể xem Cách làm cho TextField trở thành người phản hồi đầu tiên với phương pháp tương tự tại đây


Làm cách nào để thay đổi màu của ProgressView?
Bagusflyer

.progressViewStyle(CircularProgressViewStyle(tint: Color.red))sẽ thay đổi màu sắc
Bagusflyer

5

Chỉ báo tùy chỉnh

Mặc dù Apple hiện hỗ trợ Chỉ báo hoạt động gốc từ SwiftUI 2.0, Bạn có thể triển khai các hoạt ảnh của riêng mình một cách đơn giản. Tất cả đều được hỗ trợ trên SwiftUI 1.0. Ngoài ra nó đang hoạt động trong các vật dụng.

Vòng cung

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 các biến thể khác nhau Vòng cung


Thanh

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 các biến thể khác nhau Thanh


Blinkers

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 các biến thể khác nhau Blinkers


Để ngăn chặn các bức tường mã , bạn có thể tìm thấy các chỉ báo thanh lịch hơn trong kho lưu trữ trên git này .

Lưu ý rằng tất cả những hình ảnh động có một BindingPHẢI Toggle được chạy.


Điều đó thật tuyệt! Tôi đã tìm thấy một lỗi mặc dù - có một hình ảnh động thực sự xa lạ đối vớiiActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
pawello2222

Nhân tiện có vấn đề gì với iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))? @ pawello2222?
Mojtaba Hosseini

Nếu bạn đặt thành count5 hoặc ít hơn, hoạt ảnh sẽ ổn (trông giống với câu trả lời này ). Tuy nhiên, nếu bạn đặt thành count15, dấu chấm ở đầu không dừng lại ở đầu vòng tròn. Nó bắt đầu thực hiện một chu kỳ khác, sau đó quay trở lại đầu trang và sau đó bắt đầu lại chu kỳ. Tôi không chắc liệu nó có được dự định hay không. Chỉ được thử nghiệm trên trình mô phỏng, Xcode 12.0.1.
pawello2222

Hmmmm. Đó là bởi vì hình ảnh động không được đăng nhiều kỳ. Tôi nên thêm một tùy chọn tuần tự hóa vào khuôn khổ cho điều đó. Cảm ơn vì đã chia sẻ ý kiến ​​của bạn.
Mojtaba Hosseini

@MojtabaHosseini bạn làm cách nào để chuyển đổi liên kết để chạy?
GarySabo

4

Tôi đã triển khai chỉ báo UIKit cổ điển bằng SwiftUI. Xem chỉ báo hoạt động đang hoạt động tại đây

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

Ngoài câu trả lời của Mojatba Hosseini ,

Tôi đã thực hiện một vài cập nhật để có thể đưa nó vào một gói nhanh chóng :

Chỉ báo hoạt động:

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

Sự mở rộng:

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

2

Chỉ báo hoạt động trong 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()
    }
}

Chỉ báo hoạt động trong SwiftUI


2

Thử đi:

import SwiftUI

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

Thông tin thêm về 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)
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.