Activity indicator in SwiftUI

2020-02-02 06:28发布

问题:

Trying to add a full screen activity indicator in SwiftUI.

I can use .overlay(overlay: ) function in View Protocol.

With this, I can make any view overlay, but I can't find the iOS default style UIActivityIndicatorView equivalent in SwiftUI.

How can I make a default style spinner with SwiftUI?

NOTE: This is not about adding activity indicator in UIKit framework.

回答1:

Quite a few views are not yet represented in SwiftUI, but it's easily to port them into the system. You need to wrap UIActivityIndicator and make it UIViewRepresentable.

(More about this can be found in the excellent WWDC 2019 talk - 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()
    }
}

Then you can use it as follows - here's an example of a loading overlay.

Note: I prefer using ZStack, rather than overlay(:_), so I know exactly what's going on in my implementation.

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)

            }
        }
    }

}

To test it, you can use this example code:

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)
            }
        }
    }

}

Result:

Tested on Xcode 11.1



回答2:

Fully customizable Standard UIActivityIndicator in SwiftUI: (Exactly as a native View):

Base 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)
    }
}

Extension:

With this little helpful extension, you can access the configuration through a modifier like other SwiftUI views:

extension View where Self == ActivityIndicator {
    func configure(_ configuration: @escaping (Self.UIView)->Void) -> Self {
        Self.init(isAnimating: self.isAnimating, configuration: configuration)
    }
}

Customization:

You can configure it as much as you could in the original UIKit:

Exactly like SwiftUI:

ActivityIndicator(isAnimating: loading)
    .configure { $0.color = .yellow }
    .background(Color.blue)

The classic way:

Also you can configure the view in a classic initializer:

ActivityIndicator(isAnimating: loading) { (indicator: UIActivityIndicatorView) in
    indicator.color = .red
    indicator.hidesWhenStopped = false
    //Any other UIActivityIndicatorView property you like
}

Result:


This method is fully adaptable. For example you can see How to make TextField become first responder with the same method here



回答3:

If you want to a swift-ui-style solution, then this is the magic:

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
    }
  }
}

Simply to use:

ActivityIndicator()
.frame(width: 50, height: 50)

Hope it helps!



回答4:

Activity indicator in 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()
    }
}



回答5:

If you want to turn on and off network indicator you need to pass Binding

Example: SwiftUI struct where network indicator will present once customers click on the Button.

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 from Matteo so we can create network indicator from 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()
    }
}

When I want to turn on/off I am using Binding. To pass Binding from SwiftUI struct Binding<> and to change value I am using 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


标签: swift swiftui