尝试在SwiftUI中添加一个全屏活动指示器。
我可以在视图协议中使用.overlay(overlay:)函数。
有了这个,我可以做任何视图叠加,但我找不到iOS默认风格UIActivityIndicatorView等效的SwiftUI。
如何使用SwiftUI创建默认样式微调器?
注意:这不是关于在UIKit框架中添加活动指示器。
尝试在SwiftUI中添加一个全屏活动指示器。
我可以在视图协议中使用.overlay(overlay:)函数。
有了这个,我可以做任何视图叠加,但我找不到iOS默认风格UIActivityIndicatorView等效的SwiftUI。
如何使用SwiftUI创建默认样式微调器?
注意:这不是关于在UIKit框架中添加活动指示器。
当前回答
在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。
// 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)
}
}
除了Mojatba Hosseini的回答,
我做了一些更新,这样就可以把它放在一个快速包中:
活动指标:
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)
}
}
试试这个:
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中,我用ProgressView制作了这个简单易用的自定义视图
这是它的样子:
代码:
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))
}
}
}