Próbuję dodać pełnoekranowy wskaźnik aktywności w SwiftUI.
Potrafię używać .overlay(overlay: )
funkcji w View
protokole.
Dzięki temu mogę utworzyć dowolną nakładkę widoku, ale nie mogę znaleźć odpowiednika domyślnego stylu iOS UIActivityIndicatorView
w SwiftUI
.
Jak mogę utworzyć domyślny spinner w stylu SwiftUI
?
UWAGA: Nie chodzi o dodanie wskaźnika aktywności w frameworku UIKit.
Odpowiedzi:
Od wersji beta Xcode 12 ( iOS 14 ) nowy widok wywoływany
ProgressView
jest dostępny dla programistów , który może wyświetlać zarówno określony, jak i nieokreślony postęp.Jego domyślny styl
CircularProgressViewStyle
to dokładnie to, czego szukamy.var body: some View { VStack { ProgressView() // and if you want to be explicit / future-proof... // .progressViewStyle(CircularProgressViewStyle()) } }
Xcode 11.x
Sporo widoków nie jest jeszcze reprezentowanych w programie
SwiftUI
, ale łatwo jest je przenieść do systemu. Musisz to owinąćUIActivityIndicator
i zrobićUIViewRepresentable
.(Więcej na ten temat można znaleźć w doskonałym wykładzie 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() } }
Następnie możesz go użyć w następujący sposób - oto przykład nakładki ładowania.
Uwaga: wolę używać
ZStack
niżoverlay(:_)
, więc dokładnie wiem, co się dzieje w mojej implementacji.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) } } } }
Aby to przetestować, możesz użyć tego przykładowego kodu:
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) } } } }
Wynik:
źródło
isShowing: .constant(true)
. Oznacza to, że wskaźnik jest zawsze wyświetlany. To, co musisz zrobić, to mieć@State
zmienną, która jest prawdziwa, gdy chcesz, aby pojawił się wskaźnik ładowania (podczas ładowania danych), a następnie zmień to na fałsz, gdy chcesz, aby wskaźnik ładowania zniknął (po zakończeniu ładowania danych) . Jeśli na przykład zmienna jest wywoływanaisDataLoading
, zrobiłbyśisShowing: $isDataLoading
zamiast tego, gdzie wstawił MatteoisShowing: .constant(true)
.tintColor
działa tylko w czystych widokach Swift UI - nie na widokach mostkowanych (UIViewRepresentable
).Jeśli chcesz uzyskać rozwiązanie w stylu szybkiego interfejsu użytkownika , oto magia:
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 } } }
Po prostu w użyciu:
ActivityIndicator() .frame(width: 50, height: 50)
Mam nadzieję, że to pomoże!
Przykładowe zastosowanie:
ActivityIndicator() .frame(size: CGSize(width: 200, height: 200)) .foregroundColor(.orange)
źródło
iOS 14 - natywny
to tylko prosty widok.
ProgressView()
Obecnie jest to ustawienie domyślne,
CircularProgressViewStyle
ale możesz ręcznie ustawić jego styl, dodając następujący modyfikator:.progressViewStyle(CircularProgressViewStyle())
Styl może być również zgodny z
ProgressViewStyle
iOS 13 - w pełni konfigurowalny standard
UIActivityIndicator
w SwiftUI: (Dokładnie jak natywnyView
):Możesz go zbudować i skonfigurować (tak bardzo, jak w oryginale
UIKit
):ActivityIndicator(isAnimating: loading) .configure { $0.color = .yellow } // Optional configurations (🎁 bones) .background(Color.blue)
Po prostu zaimplementuj tę bazę
struct
i będziesz gotowy: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) } }
🎁 Przedłużenie kości:
Dzięki temu niewielkiemu
modifier
pomocnemu rozszerzeniu możesz uzyskać dostęp do konfiguracji za pośrednictwem podobnych interfejsów SwiftUIview
:extension View where Self == ActivityIndicator { func configure(_ configuration: @escaping (Self.UIView)->Void) -> Self { Self.init(isAnimating: self.isAnimating, configuration: configuration) } }
Klasyczny sposób:
Możesz również skonfigurować widok w klasycznym inicjatorze:
ActivityIndicator(isAnimating: loading) { $0.color = .red $0.hidesWhenStopped = false //Any other UIActivityIndicatorView property you like }
Ta metoda jest w pełni adaptowalna. Na przykład możesz zobaczyć, jak ustawić TextField jako pierwszą osobę odpowiadającą za pomocą tej samej metody tutaj
źródło
.progressViewStyle(CircularProgressViewStyle(tint: Color.red))
zmieni kolorWskaźniki niestandardowe
Chociaż Apple obsługuje teraz natywny wskaźnik aktywności z SwiftUI 2.0, możesz po prostu zaimplementować własne animacje. Wszystkie są obsługiwane w SwiftUI 1.0. Także to jest praca w widżetów.
Łuki
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) } }
Demo różnych odmian
Słupy
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)) } }
Demo różnych odmian
Klapki na oczy
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) } }
Demo różnych odmian
W celu uniknięcia ścian kodu , w tym repozytorium hostowanym na gicie można znaleźć bardziej eleganckie wskaźniki .
Należy pamiętać, że wszystkie te animacje mają
Binding
to KONIECZNIE przełącznik do uruchomienia.źródło
iActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))
przy okazji? @ pawello2222?count
5 lub mniej, animacja wygląda dobrze (wygląda podobnie do tej odpowiedzi ). Jeśli jednak ustawisz nacount
15, wiodąca kropka nie zatrzyma się na górze okręgu. Zaczyna wykonywać kolejny cykl, po czym wraca na szczyt i ponownie rozpoczyna cykl. Nie jestem pewien, czy jest to zamierzone. Przetestowano tylko na symulatorze, Xcode 12.0.1.Zaimplementowałem klasyczny wskaźnik UIKit za pomocą SwiftUI. Zobacz wskaźnik aktywności w akcji tutaj
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) } }
źródło
Oprócz Mojatba Hosseini „s odpowiedzi ,
Zrobiłem kilka aktualizacji, aby można je było szybko umieścić w pakiecie :
Wskaźnik aktywności:
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) }}
Rozbudowa:
public extension View where Self == ActivityIndicator { func configure(_ configuration: @escaping (Self.UIView) -> Void) -> Self { Self.init(isAnimating: self.isAnimating, configuration: configuration) } }
źródło
Wskaźnik aktywności w 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() } }
źródło
Spróbuj tego:
import SwiftUI struct LoadingPlaceholder: View { var text = "Loading..." init(text:String ) { self.text = text } var body: some View { VStack(content: { ProgressView(self.text) }) } }
Więcej informacji na temat w SwiftUI ProgressView
źródło
// 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) } }
źródło