Пытаюсь добавить полноэкранный индикатор активности в SwiftUI.
Я могу использовать .overlay(overlay: )
функцию в View
протоколе.
С его помощью я могу сделать любое наложение вида, но я не могу найти UIActivityIndicatorView
эквивалент стиля iOS по умолчанию в SwiftUI
.
Как я могу сделать счетчик стиля по умолчанию SwiftUI
?
ПРИМЕЧАНИЕ. Речь идет не о добавлении индикатора активности в структуру UIKit.
Ответы:
По состоянию на Xcode 12 беты ( IOS 14 ), новый вид под названием
ProgressView
является доступным для разработчиков , и которые могут отображать как детерминированные и неопределенный прогресс.Его стиль по умолчанию
CircularProgressViewStyle
равен, что мы и ищем.var body: some View { VStack { ProgressView() // and if you want to be explicit / future-proof... // .progressViewStyle(CircularProgressViewStyle()) } }
Xcode 11.x
Многие представления еще не представлены
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) } } } }
Результат:
источник
isShowing: .constant(true)
. Это означает, что индикатор отображается всегда. Что вам нужно сделать, так это иметь@State
переменную, которая имеет значение true, если вы хотите, чтобы индикатор загрузки отображался (когда данные загружаются), а затем измените ее на false, когда вы хотите, чтобы индикатор загрузки исчез (когда загрузка данных завершена) . Если,isDataLoading
например, вызывается переменная , вы бы сделали этоisShowing: $isDataLoading
вместо того, где поставил МаттеоisShowing: .constant(true)
.tintColor
работает только с чистыми представлениями пользовательского интерфейса Swift, а не с bridged (UIViewRepresentable
).Если вам нужно решение в стиле быстрого пользовательского интерфейса , то это волшебство:
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 } } }
Просто использовать:
ActivityIndicator() .frame(width: 50, height: 50)
Надеюсь, это поможет!
Пример использования:
ActivityIndicator() .frame(size: CGSize(width: 200, height: 200)) .foregroundColor(.orange)
источник
iOS 14 - Собственная
это просто простой вид.
ProgressView()
В настоящее время он используется по умолчанию,
CircularProgressViewStyle
но вы можете вручную установить его стиль, добавив следующий модификатор:.progressViewStyle(CircularProgressViewStyle())
Кроме того, стиль может быть любым, что соответствует
ProgressViewStyle
iOS 13 - полностью настраиваемый стандарт
UIActivityIndicator
в SwiftUI: (Точно как роднойView
):Вы можете собрать и настроить его (столько, сколько вы могли в оригинале
UIKit
):ActivityIndicator(isAnimating: loading) .configure { $0.color = .yellow } // Optional configurations (🎁 bones) .background(Color.blue)
Просто внедрите эту базу,
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) } }
🎁 Расширение костей:
С помощью этого небольшого полезного расширения вы можете получить доступ к конфигурации,
modifier
как и другие SwiftUIview
: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 первым респондентом с помощью того же метода здесь.
источник
.progressViewStyle(CircularProgressViewStyle(tint: Color.red))
изменит цветПользовательские индикаторы
Хотя Apple теперь поддерживает собственный индикатор активности из 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) } }
Демо разных вариаций
Бары
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 .
Обратите внимание, что у всех этих анимаций есть переключатель,
Binding
который ДОЛЖЕН запускаться.источник
iActivityIndicator(style: .rotatingShapes(count: 10, size: 15))
iActivityIndicator().style(.rotatingShapes(count: 10, size: 15))
кстати? @ pawello2222?count
значение 5 или меньше, анимация будет выглядеть нормально (похоже на этот ответ ). Однако, если вы установитеcount
значение 15, ведущая точка не остановится в верхней части круга. Он начинает выполнять другой цикл, затем возвращается к началу и затем снова запускает цикл. Я не уверен, предназначено ли это. Проверено только на симуляторе Xcode 12.0.1.Я реализовал классический индикатор UIKit с помощью SwiftUI. Посмотрите, как работает индикатор активности здесь
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) } }
источник
В дополнение к Mojatba Хоссейни ответ «S ,
Я сделал несколько обновлений, чтобы их можно было поместить в быстрый пакет :
Индикатор активности:
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) } }
источник
Индикатор активности в 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() } }
источник
Попробуй это:
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
источник
// 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) } }
источник