SwiftUI中的活动指示器

时间:2019-06-07 14:51:55

标签: swift swiftui

尝试在SwiftUI中添加全屏活动指示器。

我可以在.overlay(overlay: )协议中使用View函数。

这样,我可以进行任何视图覆盖,但是找不到UIActivityIndicatorView中与之等效的iOS默认样式SwiftUI

如何使用SwiftUI制作默认样式微调器?

注意::这与在UIKit框架中添加活动指示器无关。

16 个答案:

答案 0 :(得分:35)

SwiftUI中还没有很多视图,但是很容易将它们移植到系统中。 您需要包装UIActivityIndicator并将其包装为UIViewRepresentable

(有关此问题的更多信息,请参见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()
    }
}

然后您可以按以下方式使用它-这是加载叠加图的示例。

注意:我更喜欢使用ZStack,而不是overlay(:_),所以我确切地知道实现中发生了什么。

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)

            }
        }
    }

}

要对其进行测试,可以使用以下示例代码:

struct ContentView: View {

    var body: some View {
        LoadingView(isShowing: .constant(true)) {
            NavigationView {
                List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
                    Text(row)
                }.navigationBarTitle(Text("A List"), displayMode: .large)
            }
        }
    }

}

结果:

enter image description here

答案 1 :(得分:6)

自定义指标

尽管Apple现在从SwiftUI 2.0支持本机活动指示器,但是您可以简单地实现自己的动画。 SwiftUI 1.0均支持这些功能。它也正在在小部件中工作。

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

不同版本的演示 Arcs


酒吧

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

不同版本的演示 Bars


闪烁灯

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

不同版本的演示 Blinkers


为了防止出现代码墙,您可以在 this repo hosted on the git 中找到更优雅的指示符。

请注意,所有这些动画都有一个Binding,必须必须切换才能运行。

答案 2 :(得分:4)

我使用SwiftUI实现了经典的UIKit指标。 See the activity indicator in action here

List<WebElement> clickOnFollowButton = driver.findElements(By.xpath("//button[contains(text(),'Follow')]"));

 for (int i = 0; i < clickOnFollowButton.size() ; i++) {

    WebElement element = driver.findElements(By.xpath("//button[contains(text(),'Follow')]")).get(i);

    driver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS);

    ((JavascriptExecutor)driver).executeScript("arguments[0].scrollIntoView();", element);
    element.click();

    WebElement requested = driver.findElement(By.xpath("//button[contains(text(),'Requested')]"));
    WebElement following = driver.findElement(By.xpath("//button[contains(text(),'Following')]"));
    if(element.equals(requested) || element.equals(following)){
        continue;
    }

} 

答案 3 :(得分:3)

除了Mojatba Hosseini's answer之外,

我进行了一些更新,以便可以将其放入 swift软件包

活动指示器:

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

扩展名:

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

答案 4 :(得分:2)

如果您想使用 swift-ui-style 解决方案,那么这就是魔术:

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

简单使用:

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

希望有帮助!

答案 5 :(得分:2)

尝试:

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的更多信息

答案 6 :(得分:2)

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

enter image description here

答案 7 :(得分:1)

SwiftUI中的完全可自定义的标准UIActivityIndicator :(恰好是本机View):

基本结构:

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

扩展名:

有了这个有用的扩展,您可以像其他SwiftUI modifier一样通过view访问配置:

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

自定义:

您可以在原始UIKit中进行尽可能多的配置:

就像SwiftUI:

ActivityIndicator(isAnimating: loading)
    .configure { $0.color = .yellow }
    .background(Color.blue)

经典方式:

您还可以在经典的初始化程序中配置视图:

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

结果:

Result


此方法完全适用。例如,您可以使用相同的方法How to make TextField become first responder

来查看here

答案 8 :(得分:0)

如果要打开和关闭网络指示灯,则需要通过绑定

示例: 用户单击按钮后,SwiftUI结构将显示网络指示器。

struct SettingView: View {
    struct SettingView: View {
        @State var networkIndicator = false

var body: some View {
        ZStack {
        NavigationView() {
            List() {
Text("Item 1")
Text("Item 2")
}

        Button(action: {
                        self.purchaseManagerViewModel.restorePurchase(networkIndicator: self.$networkIndicator)
                    }) {
                        Text("Restore Purchase")
                    }
}
                    NetworkIndicatorSwiftView(isAnimating: $networkIndicator, style: .large)
}.disabled(networkIndicator)
    .blur(radius: networkIndicator ? 1.0 : 0.0)

}

Matteo的NetworkIndicatorSwiftView,因此我们可以从UIKit创建网络指示器

import Foundation
import SwiftUI
import UIKit

struct NetworkIndicatorSwiftView: UIViewRepresentable {

    @Binding var isAnimating: Bool
    let style: UIActivityIndicatorView.Style
    private static var loadingCount = 0

    func makeUIView(context: UIViewRepresentableContext<NetworkIndicatorSwiftView>) -> UIActivityIndicatorView {
        return UIActivityIndicatorView(style: style)
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<NetworkIndicatorSwiftView>) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
    }
}

当我想打开/关闭时,我正在使用绑定。 为了从SwiftUI结构Binding <>传递绑定并更改值,我正在使用networkIndicator.wrappedValue = true

func restorePurchase(networkIndicator: Binding<Bool>) {
        networkIndicator.wrappedValue = true
// Network Indicator will start
        SwiftyStoreKit.restorePurchases(atomically: true) { results in
            networkIndicator.wrappedValue = false
// Network Indicator will stop

答案 9 :(得分:0)

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

Activity indicator in SwiftUI

答案 10 :(得分: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)
    }
}

答案 11 :(得分:0)

2021-SwiftUI 2.0

ProgressView()
    .progressViewStyle(CircularProgressViewStyle())

enter image description here

答案 12 :(得分:0)

使用 SwiftUI 2.0 真的很容易我使用 ProgressView

制作了这个简单易用的自定义视图

外观如下:

enter image description here

代码:

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

答案 13 :(得分:0)

我的 2 美分用于 batuhankrbb 的漂亮和简单的代码,显示了在计时器中使用 isPresented ......或其他东西......(我将在 url 回调中使用它......)

//
//  ContentView.swift
//
//  Created by ing.conti on 27/01/21.


import SwiftUI

struct ActivityIndicatorView: View {
    @Binding var isPresented:Bool
    var body: some View {
        if isPresented{
            ZStack{
                RoundedRectangle(cornerRadius: 15).fill(Color.gray.opacity(0.1))
                ProgressView {
                    Text("Loading...")
                        .font(.title2)
                }
            }.frame(width: 120, height: 120, alignment: .center)
            .background(RoundedRectangle(cornerRadius: 25).stroke(Color.gray,lineWidth: 2))
        }
    }
}



struct ContentView: View {
    @State var isPresented = false
    @State var counter = 0
    var body: some View {
        
        VStack{
            Text("Hello, world! \(counter)")
                .padding()
            
            ActivityIndicatorView(isPresented: $isPresented)
        }.onAppear(perform: {
            _ = startRefreshing()
        })
    }
    
    
    
    func startRefreshing()->Timer{
        
        let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
            
            counter+=1
            print(counter)
            if counter>2{
                isPresented = true
            }
            
            if counter>4{
                isPresented = false
                timer.invalidate()
            }
        }
        return timer
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

答案 14 :(得分:0)

SwiftUI 中我发现有用的一种便捷方法是两步法:

  1. 创建一个 ViewModifier,将您的视图嵌入到 ZStack 中并在顶部添加进度指示器。可能是这样的:

     struct LoadingIndicator: ViewModifier {
     let width = UIScreen.main.bounds.width * 0.3
     let height =  UIScreen.main.bounds.width * 0.3
    
     func body(content: Content) -> some View {
         return ZStack {
             content
                 .disabled(true)
                 .blur(radius: 2)
    
             //gray background
             VStack{}
                 .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
                 .background(Color.gray.opacity(0.2))
                 .cornerRadius(20)
                 .edgesIgnoringSafeArea(.all)
    
             //progress indicator
             ProgressView()
                 .frame(width: width, height: height)
                 .background(Color.white)
                 .cornerRadius(20)
                 .opacity(1)
                 .shadow(color: Color.gray.opacity(0.5), radius: 4.0, x: 1.0, y: 2.0)
           }
    }
    
  2. 创建视图扩展,使条件修饰符应用程序可用于任何视图:

     extension View {
     /// Applies the given transform if the given condition evaluates to `true`.
     /// - Parameters:
     ///   - condition: The condition to evaluate.
     ///   - transform: The transform to apply to the source `View`.
     /// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
     @ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
         if condition {
             transform(self)
         } else {
             self
         }
       }
    }
    
  3. 使用非常直观。假设 myView() 返回任何您的视图。您只需使用步骤 2 中的 .if 视图扩展有条件地应用修饰符:

     var body: some View {
         myView()
           .if(myViewModel.isLoading){ view in
             view.modifier(LoadingIndicator())
         }
     }
    

如果 myViewModel.isLoading 为 false,则不会应用任何修饰符,因此不会显示加载指示器。

当然,您可以使用任何类型的进度指示器 - 默认或您自己的自定义。

答案 15 :(得分:0)

您有用于 ProgressView().progressViewStyle 修饰符,您可以在其中更改活动指示器的样式。