SwiftUI中的活动指标

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

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

有了这个,我可以使任何视图叠加,但我在SwiftUI中找不到等效的iOS默认样式UIActivityIndicatorView

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

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

131708 次浏览

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


}

结果:

enter image description here

iOS 14

这只是一个简单的观点。

ProgressView()

目前,它的默认值是CircularProgressViewStyle,但你可以通过添加以下修饰符手动设置它的样式:

.progressViewStyle(CircularProgressViewStyle())

同样,样式可以是任何符合ProgressViewStyle的东西


iOS 13及以上版本

完全自定义的标准UIActivityIndicator在SwiftUI:(完全作为一个本机View):

你可以构建和配置它(就像你在最初的UIKit中所做的那样):

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

Result


只要实现这个base struct,你就可以开始了:

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 __abc1一样通过modifier访问配置:

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

经典的方法:

你也可以在一个经典的初始化器中配置视图:

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

这种方法是完全适用的。例如,你可以用同样的方法在这里看到如何使TextField成为第一个响应

如果你想要一个swift-ui-style的解决方案,那么神奇的是:

import Foundation
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(calcScale(index: index))
.offset(y: calcYOffset(geometry))
}.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
}
}
    

func calcScale(index: Int) -> CGFloat {
return (!isAnimating ? 1 - CGFloat(Float(index)) / 5 : 0.2 + CGFloat(index) / 5)
}
    

func calcYOffset(_ geometry: GeometryProxy) -> CGFloat {
return geometry.size.width / 10 - geometry.size.height / 2
}
    

}

简单地使用:

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

希望能有所帮助!

使用示例:

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

enter image description here

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


 SwiftUI中的活动指标 .

除了Mojatba Hosseini< strong> /strong>的回答之外,

我做了一些更新,这样就可以把它放在斯威夫特包中:

活动指标:

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)
}
}
// 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)
}
}
我使用SwiftUI实现了经典的UIKit指示器。 点击这里查看活动指示器的运行情况 < / p >
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)
}
}


试试这个:

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

自定义指标

虽然苹果现在支持原生活动指示器从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 < / p >


酒吧

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 < / p >


有色眼镜

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 < / p >


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

注意,所有这些动画都有一个Binding MUST切换来运行。

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

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

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

SwiftUI中,我发现有用的一个方便的方法是2步方法:

  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,则不会应用任何修饰符,因此加载指示器将不显示。

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

你有.progressViewStyle修饰符ProgressView (),在那里你可以改变活动指示器的风格。

我已经使用AppKit和SwiftUI修改了Matteo Pacini的macOS答案。这允许你在SwiftUI中使用NSProgressIndicator,同时保留macOS 10.15的功能。

import AppKit
import SwiftUI


struct ActivityIndicator: NSViewRepresentable {
    

@Binding var isAnimating: Bool
let style: NSProgressIndicator.Style


func makeNSView(context: NSViewRepresentableContext<ActivityIndicator>) -> NSProgressIndicator {
let progressIndicator = NSProgressIndicator()
progressIndicator.style = self.style
return progressIndicator
}


func updateNSView(_ nsView: NSProgressIndicator, context: NSViewRepresentableContext<ActivityIndicator>) {
isAnimating ? nsView.startAnimation(nil) : nsView.stopAnimation(nil)
}
    

}

用法如下:

ActivityIndicator(isAnimating: .constant(true), style: .spinning)

enter image description here

基本活动指标结果:

enter image description here