Индикатор активности в SwiftUI
Попытка добавить полноэкранный индикатор активности в SwiftUI.
я могу использовать .overlay(overlay: )
функция в View
Протокол.
Благодаря этому я могу наложить любое представление, но не могу найти стиль iOS по умолчанию UIActivityIndicatorView
эквивалент в SwiftUI
,
Как я могу сделать спиннер стиля по умолчанию с SwiftUI
?
ПРИМЕЧАНИЕ. Речь идет не о добавлении индикатора активности в инфраструктуру UIKit.
17 ответов
Многие виды еще не представлены в 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)
}
}
}
}
Результат:
Обновлено для Xcode 11 beta 5
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
как и другие SwiftUI view
s:
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 первым респондентом с помощью того же метода здесь.
Если вам нужно решение в стиле быстрого пользовательского интерфейса, то это волшебство:
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)
ht tps:https://stackru.com/images/f86b7abcd84fc3cb2390d5844e9647b261b3d4cc.gif
Пользовательские индикаторы
Хотя 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
это ДОЛЖНО быть запущено.
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()
}
}
}
}
}
Индикатор активности в 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
Я нашел полезным двухэтапный подход:
Создать
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) } }
Создайте расширение представления, которое сделает приложение условного модификатора доступным для любого представления:
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 } } }
Использование очень интуитивно понятно. Предположим, что
myView()
возвращает то, что вы видите. Вы просто условно применяете модификатор, используя.if
просмотреть расширение с шага 2:var body: some View { myView() .if(myViewModel.isLoading){ view in view.modifier(LoadingIndicator()) } }
В случае, если
myViewModel.isLoading
имеет значение false, модификатор применяться не будет, поэтому индикатор загрузки отображаться не будет.
Конечно, вы можете использовать любой индикатор прогресса, какой захотите - стандартный или ваш собственный.
Я реализовал классический индикатор 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)
}
}
У вас есть модификатор .progressViewStyle для 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))
}
}
}
В дополнение к 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)
}
}
Попробуй это:
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
Я изменил ответ Маттео Пачини для macOS, используя AppKit и SwiftUI. Это позволяет вам использовать NSProgressIndicator в SwiftUI, сохраняя при этом возможности для 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)
мои 2 цента за красивый и простой код batuhankrbb, показывающий использование isPresent в таймере ... или других вещах ... (я буду использовать его в обратном вызове 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()
}
}
// 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)
}
}
Если вы хотите включать и выключать индикатор сети, вам необходимо пройти Binding
Пример: структура SwiftUI, в которой индикатор сети будет отображаться после того, как клиенты нажмут кнопку.
struct SettingView: View {
struct SettingView: View {
@State var networkIndicator = false
var body: some View {
ZStack {
NavigationView() {
List() {
Text("Item 1")
Text("Item 2")
}
Button(action: {
self.purchaseManagerViewModel.restorePurchase(networkIndicator: self.$networkIndicator)
}) {
Text("Restore Purchase")
}
}
NetworkIndicatorSwiftView(isAnimating: $networkIndicator, style: .large)
}.disabled(networkIndicator)
.blur(radius: networkIndicator ? 1.0 : 0.0)
}
NetworkIndicatorSwiftView от Matteo, чтобы мы могли создать сетевой индикатор из UIKit
import Foundation
import SwiftUI
import UIKit
struct NetworkIndicatorSwiftView: UIViewRepresentable {
@Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
private static var loadingCount = 0
func makeUIView(context: UIViewRepresentableContext<NetworkIndicatorSwiftView>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<NetworkIndicatorSwiftView>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Когда я хочу включить / выключить, я использую Binding. Чтобы передать Binding из SwiftUI struct Binding<> и изменить значение, я использую networkIndicator.wrappedValue = true
func restorePurchase(networkIndicator: Binding<Bool>) {
networkIndicator.wrappedValue = true
// Network Indicator will start
SwiftyStoreKit.restorePurchases(atomically: true) { results in
networkIndicator.wrappedValue = false
// Network Indicator will stop