The Util Designer
模仿iOS聲音控制元件
xcode 13.4.1, swift 5.5, iOS 15.4
2022-08-14
蘋果手機的控制面板大家天天都在用,只要向上一滑,就可以設置屏幕亮度、聲音大小,開啓小電筒等,這次我們就來看看如果用SwiftUI很簡單就可以實現聲音大小的控制元件。
整個元件共分三個層次,最底層是灰底,中間層就白色,用來顯示現在的音量,最上層就是一個圖示。
1. 為了更好的顯示的對比,把底色設為teal,先放最底層灰底的圓角長方形。
import SwiftUI

struct LightComponent : View {
    var body: some View {
        ZStack {
            Color.teal.ignoresSafeArea()
            ZStack {
                Rectangle()
                    .fill(.gray)
                    .cornerRadius(10)
            }.frame(width: 50, height: 200)
        }
    }
}
2. 為了更好的顯示的對比,把底色設為teal,先放最底層灰底的圓角長方形。
import SwiftUI

struct LightComponent : View {
    var body: some View {
        ZStack {
            Color.teal.ignoresSafeArea()
            ZStack {
                Rectangle()
                    .fill(.gray)
                    .cornerRadius(10)
                    .overlay(
                        Rectangle()
                            .fill(.white)
                            .cornerRadius(10)
                    )
            }.frame(width: 50, height: 200)
        }
    }
}
3. 最大聲就是全部都顯示白色,一半聲就只把下半部份顯示為白色,上半部份為灰底色,若是靜音就全部只顯示灰底色。為了達到以上的效果,可以使用clipShape和trim來裁切顯示部份,如下只顯示一半的白色,所以把trim設為0.0到0.5,:
struct ClipLine : Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: CGPoint(x: rect.width/2, y: rect.height))
            path.addLine(to: CGPoint(x: rect.width/2, y: 0))
        }
    }
}

struct LightComponent : View {
    var body: some View {
        ZStack {
            Color.teal.ignoresSafeArea()
            ZStack {
                Rectangle()
                    .fill(.gray)
                    .cornerRadius(10)
                    .overlay(
                        Rectangle()
                            .fill(.white)
                            .cornerRadius(10)
                            .clipShape(
                                ClipLine()
                                    .trim(from: 0.0, to: 0.5)
                                    .stroke(lineWidth: 100)
                            )
                    )
            }.frame(width: 50, height: 200)
        }
    }
}
4. 以下實現drag去控制白色條的增加或減少:
struct LightComponent : View {
    @State var current = 0.5
    @State var tempCurrent = 0.5
    @State var delta = 0.0
    
    var body: some View {
        ZStack {
            Color.teal.ignoresSafeArea()
            GeometryReader { proxy in
                let size = proxy.size
                let dragGesture = DragGesture()
                    .onChanged({ value in
                        delta = value.translation.height/size.height
                        tempCurrent = max(0, min(1, current - delta))
                    })
                    .onEnded({ value in
                        current = max(0, min(1, current - delta))
                        delta = 0.0
                    })
                ZStack {
                    Rectangle()
                        .fill(.gray)
                        .cornerRadius(10)
                        .overlay(
                            Rectangle()
                                .fill(.white)
                                .cornerRadius(10)
                                .clipShape(
                                    ClipLine()
                                        .trim(from: 0.0, to: current - delta)
                                        .stroke(lineWidth: 100)
                                )
                        )
                }
                .gesture(dragGesture)
            }.frame(width: 50, height: 200)
        }
    }
}
5. 最後在底部放上聲音的圖示,為了把圖示放在整個元件的底部,所以把ZStack的alignment設為bottom:
struct LightComponent : View {
    @State var current = 0.5
    @State var tempCurrent = 0.5
    @State var delta = 0.0
    
    var body: some View {
        ZStack {
            Color.teal.ignoresSafeArea()
            GeometryReader { proxy in
                let size = proxy.size
                let dragGesture = DragGesture()
                    .onChanged({ value in
                        delta = value.translation.height/size.height
                        tempCurrent = max(0, min(1, current - delta))
                    })
                    .onEnded({ value in
                        current = max(0, min(1, current - delta))
                        delta = 0.0
                    })
                ZStack(alignment: .bottom) {
                    Rectangle()
                        .fill(.gray)
                        .cornerRadius(10)
                        .overlay(
                            Rectangle()
                                .fill(.white)
                                .cornerRadius(10)
                                .clipShape(
                                    ClipLine()
                                        .trim(from: 0.0, to: current - delta)
                                        .stroke(lineWidth: 100)
                                )
                        )
                    ZStack {
                        if tempCurrent == 0.0 {
                            Image(systemName: "speaker.slash.fill")
                        }
                        if tempCurrent > 0 && tempCurrent <= 0.25 {
                            Image(systemName: "speaker.wave.1.fill")
                        }
                        if tempCurrent > 0.25 && tempCurrent < 0.75 {
                            Image(systemName: "speaker.wave.2.fill")
                        }
                        if tempCurrent >= 0.75 {
                            Image(systemName: "speaker.wave.3.fill")
                        }

                    }
                    .foregroundColor(.black)
                    .animation(.linear(duration: 0.2), value: tempCurrent)
                    .padding(.bottom, 10)
                }
                .gesture(dragGesture)
            }.frame(width: 50, height: 200)
        }
    }
}