Tentando adicionar um indicador de atividade em tela cheia no SwiftUI.
Posso usar a .overlay(overlay: )
função no View
protocolo.
Com isso, posso fazer qualquer sobreposição de visualização, mas não consigo encontrar o estilo padrão do iOS UIActivityIndicatorView
equivalente em SwiftUI
.
Como posso fazer um spinner de estilo padrão com SwiftUI
?
NOTA: Não se trata de adicionar um indicador de atividade na estrutura UIKit.
Respostas:
A partir do Xcode 12 beta ( iOS 14 ), uma nova visualização chamada
ProgressView
é disponível para os desenvolvedores e pode exibir o progresso determinado e indeterminado.Seu estilo padrão
CircularProgressViewStyle
é, que é exatamente o que estamos procurando.var body: some View { VStack { ProgressView() // and if you want to be explicit / future-proof... // .progressViewStyle(CircularProgressViewStyle()) } }
Xcode 11.x
Algumas visualizações ainda não estão representadas
SwiftUI
, mas é fácil transportá-las para o sistema. Você precisa embrulharUIActivityIndicator
e fazerUIViewRepresentable
.(Mais sobre isso pode ser encontrado na excelente palestra WWDC 2019 - Integrando 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() } }
Em seguida, você pode usá-lo da seguinte maneira - aqui está um exemplo de uma sobreposição de carregamento.
Observação: prefiro usar
ZStack
, em vez deoverlay(:_)
, portanto, sei exatamente o que está acontecendo na minha implementação.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) } } } }
Para testá-lo, você pode usar este código de exemplo:
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) } } } }
Resultado:
fonte
isShowing: .constant(true)
. Isso significa que o indicador está sempre sendo exibido. O que você precisa fazer é ter uma@State
variável que seja verdadeira quando você quiser que o indicador de carregamento apareça (quando os dados estão carregando) e, em seguida, altere-a para falsa quando quiser que o indicador de carregamento desapareça (quando os dados terminarem de carregar) . Se a variável for chamada,isDataLoading
por exemplo, você faria emisShowing: $isDataLoading
vez de onde Matteo colocouisShowing: .constant(true)
.tintColor
só funcione em visualizações de IU Swift puras - não em bridge (UIViewRepresentable
).Se você deseja uma solução no estilo swift-ui , esta é a mágica:
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 } } }
Simplesmente para usar:
ActivityIndicator() .frame(width: 50, height: 50)
Espero que ajude!
Exemplo de uso:
ActivityIndicator() .frame(size: CGSize(width: 200, height: 200)) .foregroundColor(.orange)
fonte
iOS 14 - nativo
é apenas uma visão simples.
ProgressView()
Atualmente, o padrão é,
CircularProgressViewStyle
mas você pode definir manualmente o estilo adicionando o seguinte modificador:.progressViewStyle(CircularProgressViewStyle())
Além disso, o estilo pode ser qualquer coisa que esteja em conformidade com
ProgressViewStyle
iOS 13 - Padrão totalmente personalizável
UIActivityIndicator
em SwiftUI: (Exatamente como um nativoView
):Você pode construir e configurá-lo (tanto quanto você poderia no original
UIKit
):ActivityIndicator(isAnimating: loading) .configure { $0.color = .yellow } // Optional configurations (🎁 bones) .background(Color.blue)
Basta implementar esta base
struct
e você estará pronto para prosseguir: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) } }
🎁 Extensão de ossos:
Com esta pequena extensão útil, você pode acessar a configuração por meio de um
modifier
outro SwiftUIview
s:extension View where Self == ActivityIndicator { func configure(_ configuration: @escaping (Self.UIView)->Void) -> Self { Self.init(isAnimating: self.isAnimating, configuration: configuration) } }
A maneira clássica:
Além disso, você pode configurar a visualização em um inicializador clássico:
ActivityIndicator(isAnimating: loading) { $0.color = .red $0.hidesWhenStopped = false //Any other UIActivityIndicatorView property you like }
Este método é totalmente adaptável. Por exemplo, você pode ver Como fazer TextField se tornar o primeiro a responder com o mesmo método aqui
fonte
.progressViewStyle(CircularProgressViewStyle(tint: Color.red))
mudará a corIndicadores Customizados
Embora a Apple suporte o indicador de atividade nativo agora do SwiftUI 2.0, você pode simplesmente implementar suas próprias animações. Todos eles são suportados no SwiftUI 1.0. Também é funcionando em widgets.
Arcos
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) } }
Demonstração de diferentes variações
Barras
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)) } }
Demonstração de diferentes variações
Bloqueios
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) } }
Demonstração de diferentes variações
Para evitar paredes de código , você pode encontrar indicadores mais elegantes neste repo hospedado no git .
Observe que todas essas animações têm um
Binding
que DEVE alternar para serem executadas.fonte
iActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))
a propósito? @ pawello2222?count
para 5 ou menos, a animação parece boa (é semelhante a esta resposta ). No entanto, se você definir ocount
para 15, o ponto inicial não pára no topo do círculo. Ele começa a fazer outro ciclo, depois volta ao topo e então começa o ciclo novamente. Não tenho certeza se foi intencional. Testado apenas no simulador, Xcode 12.0.1.Implementei o indicador clássico UIKit usando SwiftUI. Veja o indicador de atividade em ação aqui
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) } }
fonte
Além Mojatba Hosseini resposta 's ,
Fiz algumas atualizações para que isso possa ser colocado em um pacote rápido :
Indicador de atividade:
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) }}
Extensão:
public extension View where Self == ActivityIndicator { func configure(_ configuration: @escaping (Self.UIView) -> Void) -> Self { Self.init(isAnimating: self.isAnimating, configuration: configuration) } }
fonte
Indicador de atividade em 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() } }
fonte
Experimente isto:
import SwiftUI struct LoadingPlaceholder: View { var text = "Loading..." init(text:String ) { self.text = text } var body: some View { VStack(content: { ProgressView(self.text) }) } }
Mais informações sobre em SwiftUI ProgressView
fonte
// 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) } }
fonte
2021 - SwiftUI 2.0
ProgressView() .progressViewStyle(CircularProgressViewStyle())
fonte
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() } } } } }
fonte
É realmente fácil com o SwiftUI 2.0. Criei essa visualização personalizada simples e fácil com
ProgressView
É assim que parece: Aqui
Código:
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)) } } }
fonte