The Util Designer
SwiftUI 創建Capsule Loading動畫
xcode 13.3.1, swift 5.5, iOS 15.4
2022-08-10
進度圈在很多地方都會用到,這篇我們會講講怎樣用SwiftUI Capsule Loading動畫。
1. 先創建一個小小的Capsule。
struct CapsuleExample : View {
    var body: some View {
        Capsule()
            .stroke(.black, lineWidth: 5.0)
            .frame(width: 20, height: 50)
    }
}
2. 把Capsule向下移80個單位,並轉成一定的角度。
struct CapsuleExample : View {
    
    let degree : Double = 30.0
    
    var body: some View {
        Capsule()
            .stroke(.black, lineWidth: 5.0)
            .frame(width: 20, height: 50)
            .offset(y: 80)
            .rotationEffect(.degrees(degree))
    }
    
}
3. 把這個包裝 成一個Component組件 - CapsulePiece。
struct CapsulePiece : View {
    let degree : Double
    
    var body: some View {
        Capsule()
            .stroke(.black, lineWidth: 5.0)
            .frame(width: 20, height: 50)
            .offset(y: 80)
            .rotationEffect(.degrees(degree))
    }
}

struct CapsuleExample: View {
    
    let degree : Double = 30.0
    
    var body: some View {
        CapsulePiece(degree: degree)
    }
    
}
4. 覆用CapsulePiece組裝成一圈。
struct CapsulePiece : View {
    let degree : Double
    
    var body: some View {
        Capsule()
            .stroke(.black, lineWidth: 5.0)
            .frame(width: 20, height: 50)
            .offset(y: 80)
            .rotationEffect(.degrees(degree))
    }
}
struct CapsuleExample: View {
    
    let numOfPieces = 15
    
    var body: some View {
        ZStack {
            ForEach(0..<numOfPieces, id:\.self) { i in
                CapsulePiece(degree: 360.0/Double(numOfPieces)*Double(i))
            }
        }
    }
    
}
這個公式是計算每個CapsulePiece所需轉的角度:360.0/Double(numOfPieces)*Double(i)。
5. 為了可以做的動畫,需要在CapsulePiece增加一個變量selectFlag,以便控制其顯示或隱藏,以下例子我們只顯示第一個CapsulePiece控件,其他都隱藏。大家可以試試改變currentIndex的值去控制只顯示第幾個CapsulePiece控件。
struct CapsulePiece : View {
    let degree : Double
    let selectFlag : Bool
    var body: some View {
        Capsule()
            .stroke(.black.opacity(selectFlag ? 1.0 : 0.0), lineWidth: 5.0)
            .frame(width: 20, height: 50)
            .offset(y: 80)
            .rotationEffect(.degrees(degree))
    }
}
struct CapsuleExample: View {
    
    let numOfPieces = 15
    @State var currentIndex = 0
    
    var body: some View {
        ZStack {
            ForEach(0..<numOfPieces, id:\.self) { i in
                CapsulePiece(degree: 360.0/Double(numOfPieces)*Double(i), selectFlag: i==currentIndex)
            }
        }
    }
}
6. 變量currentIndex可以用來控制顯示第幾個CapsulePiece。只要增加一個Timer在隔一隔時間就把currentIndex增加一,就可以做成動畫。
struct CapsulePiece : View {
    let degree : Double
    let selectFlag : Bool
    var body: some View {
        Capsule()
            .stroke(.black.opacity(selectFlag ? 1.0 : 0.0), lineWidth: 5.0)
            .frame(width: 20, height: 50)
            .offset(y: 80)
            .rotationEffect(.degrees(degree))
    }
}
struct CapsuleExample: View {
    
    let numOfPieces = 15
    @State var currentIndex = 0
    
    var body: some View {
        ZStack {
            ForEach(0..<numOfPieces, id:\.self) { i in
                CapsulePiece(degree: 360.0/Double(numOfPieces)*Double(i), selectFlag: i==currentIndex)
            }
        }
        .onAppear(perform: animate)
    }
    
    func animate() {
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            currentIndex = (currentIndex + 1) % numOfPieces
        }
    }
}
7. 以下把控件CapsulePiece的顯示和隱藏交替做成漸變的方式。而做漸變動畫在SwiftUI只要告訴animation就可以,實作以下:
struct CapsulePiece : View {
    let degree : Double
    let selectFlag : Bool
    var body: some View {
        Capsule()
            .stroke(.black.opacity(selectFlag ? 1.0 : 0.0), lineWidth: 5.0)
            .frame(width: 20, height: 50)
            .offset(y: 80)
            .rotationEffect(.degrees(degree))
            .animation(.linear(duration: 0.5), value: selectFlag)
    }
}
struct CapsuleExample: View {
    
    let numOfPieces = 15
    @State var currentIndex = 0
    
    var body: some View {
        ZStack {
            ForEach(0..<numOfPieces, id:\.self) { i in
                CapsulePiece(degree: 360.0/Double(numOfPieces)*Double(i), selectFlag: i==currentIndex)
            }
        }
        .onAppear(perform: animate)
    }
    
    func animate() {
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            currentIndex = (currentIndex + 1) % numOfPieces
        }
    }
}