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

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

有了这个,我可以做任何视图叠加,但我找不到iOS默认风格UIActivityIndicatorView等效的SwiftUI。

如何使用SwiftUI创建默认样式微调器?

注意:这不是关于在UIKit框架中添加活动指示器。


当前回答

我的2美分的漂亮和更简单的代码的batuhankrbb,显示使用定时器中的ispresents…或者其他东西…(我将使用它在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()
    }
}

其他回答

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

从Xcode 12 beta版(iOS 14)开始,开发者可以使用一个名为ProgressView的新视图,它可以显示确定进度和不确定进度。

它的样式默认为CircularProgressViewStyle,这正是我们正在寻找的。

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

Xcode 11.倍

相当多的视图还没有在SwiftUI中表示,但很容易将它们移植到系统中。 你需要包装UIActivityIndicator并使其UIViewRepresentable。

(关于这方面的更多信息,可以在WWDC 2019的精彩演讲-集成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"], id: \.self) { row in
                    Text(row)
                }.navigationBarTitle(Text("A List"), displayMode: .large)
            }
        }
    }

}

结果:

在SwiftUI中,我发现一个方便的方法是两步方法:

Create a ViewModifier that will embed your view into ZStack and add progress indicator on top. Could be something like this: 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) } } Create view extension that will make conditional modifier application available to any view: 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 } } } Usage is very intuitive. Suppose that myView() returns whatever your view is. You just conditionally apply the modifier using .if view extension from step 2: var body: some View { myView() .if(myViewModel.isLoading){ view in view.modifier(LoadingIndicator()) } }

如果是myViewModel。isLoading为false,没有修饰符将被应用,因此加载指示器将不显示。

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

自定义指标

虽然苹果现在支持原生活动指示器从SwiftUI 2.0,你可以简单地实现自己的动画。这些都是SwiftUI 1.0所支持的。此外,它还在小部件方面发挥作用。

Arcs

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

不同变奏的演示


Bars

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

不同变奏的演示


眼罩

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

不同变奏的演示


为了防止代码墙,您可以在git托管的这个repo中找到更优雅的指示器。

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

试试这个:

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