Last active
May 16, 2020 18:53
-
-
Save mattyoung/c26f1daa637732801a0b26cd0e510b8f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Note: | |
- To disable all shadows, comment out all .modifier(Emboss()) and .modifier(Deboss()) | |
- Search for .drawingGroup(), uncomment it to see it kills all shadows | |
*/ | |
import SwiftUI | |
// Shadows kills performance? Comment this out and comment out all the call to this modifiers to disable | |
fileprivate struct Emboss: ViewModifier { | |
func body(content: Content) -> some View { | |
content | |
.shadow(color: .black, radius: 1, x: -1, y: -1) | |
.shadow(color: .gray, radius: 0.5, x: 1, y: 1) | |
} | |
} | |
fileprivate struct Deboss: ViewModifier { | |
func body(content: Content) -> some View { | |
content | |
.shadow(color: .black, radius: 1, x: 1, y: 1) | |
.shadow(color: Color(red: 170 / 255, green: 170 / 255, blue: 170 / 255), radius: 0.5, x: -1, y: -1) | |
} | |
} | |
// Hack for a left or right justified label (not used right now, using a different way to do justified label hoping it's faster) | |
// Instead, used one hidden Text() in the top of verticalBody(), get the width there and use that width value with Text().preference() | |
// then use this width value directly on each Text(). | |
// Which way is faster? Using this JustifiedLabel() or | |
fileprivate struct JustifiedLabel: View { | |
let text: String | |
let alignment: HorizontalAlignment | |
let width: String | |
let font: Font | |
let color: Color | |
var body: some View { | |
VStack(alignment: alignment) { | |
// use an invisible Text to get the uniform width | |
Text(width) | |
.font(font) | |
.frame(height: 0) | |
.hidden() | |
Text(text) | |
.font(font) | |
.foregroundColor(color) | |
.modifier(Emboss()) | |
} | |
} | |
} | |
extension CGSize { | |
var edgeLength: CGFloat { width + height } | |
} | |
struct AudioChannelsMeter: Animatable, View { | |
static private let displayBackgroundColor = Color(red: 99 / 255, green: 99 / 255, blue: 99 / 255) | |
static private let levelBarBackgroundColor = Color(red: 69 / 255, green: 50 / 255, blue: 46 / 255) | |
// default colors scheme from level 0 to n, this array size determines how many bars the audio meter has | |
static private let defaultColors = [ | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 123 / 255, green: 176 / 255, blue: 140 / 255), | |
Color(red: 247 / 255, green: 218 / 255, blue: 185 / 255), | |
Color(red: 247 / 255, green: 218 / 255, blue: 185 / 255), | |
Color(red: 232 / 255, green: 170 / 255, blue: 152 / 255), | |
Color(red: 242 / 255, green: 92 / 255, blue: 67 / 255), | |
] | |
static private let labelColor = Color(red: 190 / 255, green: 190 / 255, blue: 190 / 255) | |
enum Orientation { | |
case horizontal | |
case vertical | |
} | |
let orientation: Orientation | |
let levelColors: [Color] | |
let labelDigitWidthString: String // A String of "8" for deriving the label width | |
// this represent volume: first is left, second is right, must use Double due to VectorArithmatic requirement, too bad cannot be Int! | |
var animatableData = AnimatablePair(1.0, 1.0) | |
/// Create an audio channels meter view | |
/// - Parameters: | |
/// - orientation: .vertical or .horizontal | |
/// - colors: An array of colors representing the meter's color bars | |
/// - leftVolume: The left channel's volume value 0 to 1 | |
/// - rightVolume: The right channel's volume value 0 to 1 | |
init(orientation: Orientation = .vertical, colors: [Color] = Self.defaultColors, leftVolume: Double, rightVolume: Double) { | |
self.orientation = orientation | |
self.levelColors = colors | |
self.labelDigitWidthString = String(repeating: "8", count: String(colors.count).count) | |
self.animatableData = AnimatablePair(leftVolume, rightVolume) | |
} | |
/// Generates the bar graphic for the level. The same is used to level 1 - 9 and 10 | |
/// only difference is the onView closure of how the color view is created | |
/// | |
/// - Parameters: | |
/// - level: the level this bar represent | |
/// - volume: the volume value the audio level is at (0 - 1) (is driven by animatableData) | |
/// - onView: the closure that creates the color view for the level | |
/// - Returns: A view representing this level bar graphic | |
func levelBar<V: View>(for level: Int, volume: Double, cornerRadius: CGFloat, onView: (Color, Bool) -> V) -> some View { | |
ZStack { | |
Self.levelBarBackgroundColor | |
// volume == 0: all dark nothing is on, volume < 0.1 1, volume < 0.2 2, etc | |
// Scale by Self.levelColors.count so we can have any number of bars | |
onView(self.levelColors[level - 1], volume > 0 && Int(volume * Double(self.levelColors.count)) + 1 >= level) | |
} | |
.cornerRadius(cornerRadius) | |
.modifier(Emboss()) | |
} | |
/// create the on state color view for level | |
/// turn on immediately, then when off, do fade out animation | |
func levelColor(color: Color, isOn: Bool) -> some View { | |
color | |
.opacity(isOn ? 1 : 0) | |
} | |
/// create the on state color view for the max level, instant on, fade out | |
func maxLevelColor(color: Color, isOn: Bool) -> some View { | |
color | |
.opacity(isOn ? 1 : 0) | |
.animation(isOn ? nil : .linear(duration: 1.5)) | |
} | |
// consolidate compute of horizontal body metrics in this one place for re-use in both left-to-right and right-to-left versions | |
func computeHorizontalMetrics(_ proxy: GeometryProxy) -> (spacing: CGFloat, barCornerRadius: CGFloat, displayCornerRadius: CGFloat, labelFont: Font, showLabel: Bool, leftRightFont: Font) { | |
return ( | |
spacing : min(proxy.size.width, proxy.size.height) / 25, | |
barCornerRadius : proxy.size.edgeLength / 200, | |
displayCornerRadius : proxy.size.edgeLength / 80, | |
labelFont : Font.system(size: min(proxy.size.height / 10, proxy.size.width / CGFloat(self.levelColors.count + 1) / 2.1)), | |
showLabel : min(proxy.size.height / 10, proxy.size.width / CGFloat(self.levelColors.count + 1) / 2.1) >= 7, | |
// compute a larger font for "L"/"R" channel display so it's not too small | |
leftRightFont : Font.system(size: max(proxy.size.height / 10, proxy.size.width / CGFloat(self.levelColors.count + 1) / 5)) | |
) | |
} | |
// MARK: == horizontal body | |
// Use this func to get around "Generic parameter 'Content' could not be inferred" | |
func horizontalLeftToRightBody(_ proxy: GeometryProxy) -> some View { | |
let metrics = computeHorizontalMetrics(proxy) | |
return HStack(spacing: metrics.spacing) { | |
VStack(spacing: metrics.spacing) { | |
// invisible for spacing purpose | |
if metrics.showLabel { | |
Text(verbatim: "1") | |
.font(metrics.labelFont) | |
.opacity(0) | |
} | |
VStack { | |
Spacer() | |
Text("L") | |
.font(metrics.leftRightFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.rotationEffect(.degrees(90)) | |
Spacer() | |
} | |
VStack { | |
Spacer() | |
Text("R") | |
.font(metrics.leftRightFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.rotationEffect(.degrees(90)) | |
Spacer() | |
} | |
// invisible for spacing purpose | |
if metrics.showLabel { | |
Text(verbatim: "1") | |
.font(metrics.labelFont) | |
.opacity(0) | |
} | |
} | |
.padding(.leading, metrics.spacing) | |
// .layoutPriority(1) // raise the priority to make this take up as much room as it needs | |
.fixedSize(horizontal: true, vertical: false) | |
ForEach(1...self.levelColors.count - 1, id: \.self) { level in | |
VStack(spacing: metrics.spacing) { | |
if metrics.showLabel { | |
Text(String(level)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.bottom, metrics.spacing / 2) | |
} | |
self.levelBar(for: level, volume: self.animatableData.first, cornerRadius: metrics.barCornerRadius, onView: self.levelColor) | |
self.levelBar(for: level, volume: self.animatableData.second, cornerRadius: metrics.barCornerRadius, onView: self.levelColor) | |
if metrics.showLabel { | |
Text(String(level)) | |
.font(metrics.labelFont) | |
.allowsTightening(true) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.top, metrics.spacing / 2) | |
} | |
} | |
} | |
// Spacial case for level last volume level: | |
VStack(spacing: metrics.spacing) { | |
if metrics.showLabel { | |
Text(String(self.levelColors.count)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.bottom, metrics.spacing / 2) | |
} | |
self.levelBar(for: self.levelColors.count, volume: self.animatableData.first, cornerRadius: metrics.barCornerRadius, onView: maxLevelColor) | |
self.levelBar(for: self.levelColors.count, volume: self.animatableData.second, cornerRadius: metrics.barCornerRadius, onView: maxLevelColor) | |
if metrics.showLabel { | |
Text(String(self.levelColors.count)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.top, metrics.spacing / 2) | |
} | |
} | |
} | |
.padding(.all, metrics.spacing) | |
.padding(.trailing, metrics.spacing) | |
.background(Self.displayBackgroundColor) | |
.cornerRadius(metrics.displayCornerRadius) | |
.modifier(Deboss()) | |
} | |
func horizontalRightToLeftBody(_ proxy: GeometryProxy) -> some View { | |
let metrics = computeHorizontalMetrics(proxy) | |
return HStack(spacing: metrics.spacing) { | |
// Spacial case for level last volume level: | |
VStack(spacing: metrics.spacing) { | |
if metrics.showLabel { | |
Text(String(self.levelColors.count)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.bottom, metrics.spacing / 2) | |
} | |
self.levelBar(for: self.levelColors.count, volume: self.animatableData.second, cornerRadius: metrics.barCornerRadius, onView: maxLevelColor) | |
self.levelBar(for: self.levelColors.count, volume: self.animatableData.first, cornerRadius: metrics.barCornerRadius, onView: maxLevelColor) | |
if metrics.showLabel { | |
Text(String(self.levelColors.count)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.top, metrics.spacing / 2) | |
} | |
} | |
ForEach((1...self.levelColors.count - 1).reversed(), id: \.self) { level in | |
VStack(spacing: metrics.spacing) { | |
if metrics.showLabel { | |
Text(String(level)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.bottom, metrics.spacing / 2) | |
} | |
self.levelBar(for: level, volume: self.animatableData.second, cornerRadius: metrics.barCornerRadius, onView: self.levelColor) | |
self.levelBar(for: level, volume: self.animatableData.first, cornerRadius: metrics.barCornerRadius, onView: self.levelColor) | |
if metrics.showLabel { | |
Text(String(level)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.padding(.top, metrics.spacing / 2) | |
} | |
} | |
} | |
VStack(spacing: metrics.spacing) { | |
// invisible for spacing purpose | |
if metrics.showLabel { | |
Text(verbatim: "1") | |
.font(metrics.labelFont) | |
.opacity(0) | |
} | |
VStack { | |
Spacer() | |
Text("R") | |
.font(metrics.leftRightFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.rotationEffect(.degrees(-90)) | |
Spacer() | |
} | |
VStack { | |
Spacer() | |
Text("L") | |
.font(metrics.leftRightFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.rotationEffect(.degrees(-90)) | |
Spacer() | |
} | |
// invisible for spacing purpose | |
if metrics.showLabel { | |
Text(verbatim: "1") | |
.font(metrics.labelFont) | |
.opacity(0) | |
} | |
} | |
.padding(.trailing, metrics.spacing) | |
// .layoutPriority(1) // raise the priority to force this to take up as much room as it needs | |
.fixedSize(horizontal: true, vertical: false) | |
} | |
.padding(.all, metrics.spacing) | |
.padding(.leading, metrics.spacing) | |
.background(Self.displayBackgroundColor) | |
.cornerRadius(metrics.displayCornerRadius) | |
.modifier(Deboss()) | |
} | |
fileprivate struct CGFloatPreferenceKey: PreferenceKey { | |
static var defaultValue: CGFloat = 0 | |
static func reduce(value: inout Value, nextValue: () -> Value) { | |
value = nextValue() | |
} | |
} | |
// how wide are the labels? computed with a invisible Text().preference() | |
@State private var labelWidth: CGFloat? = nil | |
// MARK: == vertical body | |
func verticalBody(_ proxy: GeometryProxy) -> some View { | |
let metrics = ( | |
spacing : min(proxy.size.width, proxy.size.height) / 40, | |
barCornerRadius : proxy.size.edgeLength / 200, | |
displayCornerRadius : proxy.size.edgeLength / 80, | |
labelFont : Font.system(size: proxy.size.height / CGFloat(self.levelColors.count + 1) / 2), | |
showLabel : proxy.size.height / CGFloat(self.levelColors.count + 1) / 2 >= 7, | |
// compute a larger font for "L"/"R" channel display | |
leftRightFont : Font.system(size: max(proxy.size.width / 28, proxy.size.height / CGFloat(self.levelColors.count + 1) / 2.1)) | |
) | |
return VStack(spacing: metrics.spacing) { | |
// use an invisible 0 height Text() just to know the label width | |
// doing this to pre-calculate each label's width, avoiding overhead compute on each justified label Text() | |
// this caused extra in between spacing, so we don't add padding(.top) | |
// | |
// Is this way faster or use JustifiedLabel with extra invisible Text() on every label? | |
Text(self.labelDigitWidthString) | |
.font(metrics.labelFont) | |
// which way is better? Using .hidden() now. | |
// .opacity(0) | |
.background( | |
GeometryReader { | |
Color.clear | |
.preference(key: CGFloatPreferenceKey.self, value: $0.size.width) | |
} | |
) | |
// hidden? or opacity(0)? | |
.hidden() | |
.frame(height: 0) | |
.onPreferenceChange(CGFloatPreferenceKey.self) { self.labelWidth = $0 } | |
// Spacial case for max volume level: | |
HStack(spacing: metrics.spacing) { | |
if metrics.showLabel { | |
// JustifiedLabel(text: String(self.levelColors.count), alignment: .trailing, width: self.labelDigitWidthString, font: metrics.labelFont, color: Self.textColor) | |
Text(String(self.levelColors.count)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.frame(width: self.labelWidth, alignment: .trailing) | |
.padding(.trailing, metrics.spacing / 2) | |
} | |
self.levelBar(for: self.levelColors.count, volume: self.animatableData.first, cornerRadius: metrics.barCornerRadius, onView: maxLevelColor) | |
self.levelBar(for: self.levelColors.count, volume: self.animatableData.second, cornerRadius: metrics.barCornerRadius, onView: maxLevelColor) | |
if metrics.showLabel { | |
// JustifiedLabel(text: String(self.levelColors.count), alignment: .leading, width: self.labelDigitWidthString, font: metrics.labelFont, color: Self.textColor) | |
Text(String(self.levelColors.count)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.frame(width: self.labelWidth, alignment: .leading) | |
.padding(.leading, metrics.spacing / 2) | |
} | |
} | |
ForEach((1...self.levelColors.count - 1).reversed(), id: \.self) { level in | |
HStack(spacing: metrics.spacing) { | |
if metrics.showLabel { | |
// JustifiedLabel(text: String(level), alignment: .trailing, width: self.labelDigitWidthString, font: metrics.labelFont, color: Self.textColor) | |
Text(String(level)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.frame(width: self.labelWidth, alignment: .trailing) | |
.padding(.trailing, metrics.spacing / 2) | |
} | |
self.levelBar(for: level, volume: self.animatableData.first, cornerRadius: metrics.barCornerRadius, onView: self.levelColor) | |
self.levelBar(for: level, volume: self.animatableData.second, cornerRadius: metrics.barCornerRadius, onView: self.levelColor) | |
if metrics.showLabel { | |
// JustifiedLabel(text: String(level), alignment: .leading, width: self.labelDigitWidthString, font: metrics.labelFont, color: Self.textColor) | |
Text(String(level)) | |
.font(metrics.labelFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
.frame(width: self.labelWidth, alignment: .leading) | |
.padding(.leading, metrics.spacing / 2) | |
} | |
} | |
} | |
HStack(spacing: metrics.spacing) { | |
// invisible for spacing purpose | |
if metrics.showLabel { | |
// Text(self.labelDigitWidthString).font(metrics.labelFont).opacity(0) | |
Color.clear | |
.frame(width: self.labelWidth) | |
.padding(.trailing, metrics.spacing / 2) | |
} | |
HStack { | |
Spacer() | |
Text("L") | |
.font(metrics.leftRightFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
Spacer() | |
} | |
HStack { | |
Spacer() | |
Text("R") | |
.font(metrics.leftRightFont) | |
.foregroundColor(Self.labelColor) | |
.modifier(Emboss()) | |
Spacer() | |
} | |
// invisible for spacing purpose | |
if metrics.showLabel { | |
// Text(self.labelDigitWidthString).font(metrics.labelFont).opacity(0) | |
Color.clear | |
.frame(width: self.labelWidth) | |
.padding(.leading, metrics.spacing / 2) | |
} | |
} | |
// .layoutPriority(1) // raise the priority to force this to take up as much room as it needs | |
.fixedSize(horizontal: false, vertical: true) | |
} | |
.padding(.all, metrics.spacing) | |
// the invisible Text() up top the calculate label width cause extra space, so we don't need extra padding here | |
// add this padding back if use the JustifiedLabel() hack | |
// .padding(.top, metrics.spacing) | |
.background(Self.displayBackgroundColor) | |
.cornerRadius(metrics.displayCornerRadius) | |
.modifier(Deboss()) | |
} | |
@Environment(\.layoutDirection) var layoutDirection | |
var body: some View { | |
GeometryReader { | |
if self.orientation == .vertical { | |
self.verticalBody($0) | |
} else if self.layoutDirection == .leftToRight { | |
// deal with layoutDirection with specific horizontal layout scheme | |
// left-to-right: left on top, right on bottom, right-to-left right on top, left on bottom | |
// so each is compose differently insize HStack, cannot be just flipped | |
self.horizontalLeftToRightBody($0) | |
} else { | |
self.horizontalRightToLeftBody($0) | |
} | |
} | |
.lineLimit(1) // set this way out here to make all Text()'s single line | |
// .drawingGroup() // adding this kills all the shadow effect!!! | |
// force left-to-right layout because we do not want flip on RTL layout, we explicitly handle layout directly in our own way | |
.environment(\.layoutDirection, .leftToRight) | |
} | |
} | |
// MARK: ======================== For Preview ======================== | |
let gunMetal = [ | |
Color(#colorLiteral(red: 0.3580220342, green: 0.5452805758, blue: 0.5934882164, alpha: 1)), Color(#colorLiteral(red: 0, green: 0.5804002881, blue: 0.5930590034, alpha: 1)), Color(#colorLiteral(red: 0, green: 0.5492544174, blue: 0.5956115723, alpha: 1)), Color(#colorLiteral(red: 0, green: 0.5911649466, blue: 0.5927722454, alpha: 1)), | |
Color(#colorLiteral(red: 0.503729105, green: 0.5897772908, blue: 0, alpha: 1)), Color(#colorLiteral(red: 0.5556029081, green: 0.5882229209, blue: 0.09041626006, alpha: 1)), Color(#colorLiteral(red: 0.5494605899, green: 0.5884171128, blue: 0, alpha: 1)), Color(#colorLiteral(red: 0.6841781139, green: 0.6800169349, blue: 0, alpha: 1)), Color(#colorLiteral(red: 0.6841781139, green: 0.6800169349, blue: 0, alpha: 1)), Color(#colorLiteral(red: 0.6841781139, green: 0.6800169349, blue: 0, alpha: 1)), Color(#colorLiteral(red: 0.5650770068, green: 0.5879210234, blue: 0.4133812189, alpha: 1)), Color(#colorLiteral(red: 0.5877895951, green: 0.5839053988, blue: 0.2975866795, alpha: 1)), | |
Color(#colorLiteral(red: 0.4171827435, green: 0.2856318951, blue: 0.6059355736, alpha: 1)), Color(#colorLiteral(red: 0.4088526666, green: 0.1750936508, blue: 0.6087446809, alpha: 1)), Color(#colorLiteral(red: 0.4514952898, green: 0.1621929407, blue: 0.6084524393, alpha: 1)), Color(#colorLiteral(red: 0.4549749494, green: 0.07350998372, blue: 0.6094855666, alpha: 1)), Color(#colorLiteral(red: 0.529676199, green: 0.2100917697, blue: 0.5733132362, alpha: 1)), Color(#colorLiteral(red: 0.5142193437, green: 0.1696169972, blue: 0.6074476838, alpha: 1)), Color(#colorLiteral(red: 0.5326375365, green: 0.01360050589, blue: 0.6087265015, alpha: 1)), | |
Color(#colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)), Color(#colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1)), Color(#colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1)), | |
] | |
let rosie = [ | |
Color(#colorLiteral(red: 0.7254902124, green: 0.4784313738, blue: 0.09803921729, alpha: 1)), Color(#colorLiteral(red: 0.9529411793, green: 0.6862745285, blue: 0.1333333403, alpha: 1)), Color(#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)), Color(#colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)), Color(#colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)), | |
Color(#colorLiteral(red: 0.4392156899, green: 0.01176470611, blue: 0.1921568662, alpha: 1)), Color(#colorLiteral(red: 0.5725490451, green: 0, blue: 0.2313725501, alpha: 1)), Color(#colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1)), Color(#colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1)), Color(#colorLiteral(red: 0.9098039269, green: 0.4784313738, blue: 0.6431372762, alpha: 1)), | |
] | |
struct VolumeDatum { | |
var left = 0.8 | |
var right = 0.2 | |
mutating func newValue() { | |
left = Double.random(in: -0.1...1.1) | |
right = Double.random(in: -0.1...1.1) | |
} | |
} | |
extension Array { | |
mutating func mutateEach( _ body: (inout Element) -> ()){ | |
for index in self.indices { | |
body( &self[index] ) | |
} | |
} | |
} | |
struct AudioChannelsDemo: View { | |
// Source for valume data so that each meter is different | |
@State private var volumeData = [VolumeDatum](repeating: .init(), count: 5) | |
@State private var manualInput = 0.0 | |
static private let updatePeriod = 0.2 | |
@State private var randomWalk = false // { didSet { toggleTimer() } } // didSet on @State do not fire, unlike ObservableObject | |
@State private var timer: Timer? | |
var body: some View { | |
ZStack { | |
Color.gray | |
VStack(spacing: 0) { | |
VStack { | |
HStack { | |
VStack { | |
AudioChannelsMeter(colors: rosie, leftVolume: self.volumeData[0].left, rightVolume: self.volumeData[0].right) | |
AudioChannelsMeter(leftVolume: self.volumeData[1].left, rightVolume: self.volumeData[1].right) | |
} | |
AudioChannelsMeter(colors: gunMetal, leftVolume: self.volumeData[2].left, rightVolume: self.volumeData[2].right) | |
} | |
HStack { | |
AudioChannelsMeter(orientation: .horizontal, colors: rosie, leftVolume: self.volumeData[3].left, rightVolume: self.volumeData[3].right) | |
AudioChannelsMeter(orientation: .horizontal, leftVolume: self.volumeData[4].left, rightVolume: self.volumeData[4].right) | |
} | |
.frame(height: 100) | |
} | |
.padding() | |
.animation(.linear(duration: Self.updatePeriod / 2)) | |
VStack(spacing: 1) { | |
Toggle(isOn: Binding(get: { self.randomWalk }, set: { self.randomWalk = $0; self.toggleTimer() })) { | |
// https://forums.swift.org/t/swift-5-2-struct-property-wrapper-didset-defect/34403/12 | |
// Toggle(isOn: self.$randomWalk) { | |
Text("Random") | |
} | |
.padding(.bottom, self.randomWalk ? 15 : 0) | |
if !self.randomWalk { | |
Slider(value: self.$manualInput, | |
in: 0...1, | |
step: 0.01, | |
minimumValueLabel: Text("0"), | |
maximumValueLabel: Text("1") | |
) { | |
Text("blah blah why not show up somethere?") | |
} | |
HStack { | |
Text("Volume \(self.manualInput, specifier: "%.2f")") | |
Button("Fire") { | |
self.volumeData.mutateEach { e in | |
e.left = self.manualInput | |
e.right = self.manualInput | |
} | |
} | |
.padding(10) | |
.background(Color.orange) | |
.cornerRadius(10) | |
.padding() | |
} | |
} | |
} | |
.padding(5) | |
.background(Color.yellow) | |
} | |
} | |
.edgesIgnoringSafeArea([.leading, .bottom, .trailing]) | |
} | |
func randomize() { | |
self.volumeData.mutateEach { | |
$0.newValue() | |
} | |
} | |
func toggleTimer() { | |
if randomWalk, timer == nil { | |
randomize() | |
timer = Timer.scheduledTimer( | |
withTimeInterval: Self.updatePeriod, repeats: true | |
) { _ in | |
self.randomize() | |
} | |
} else if !randomWalk, let timer = self.timer { | |
timer.invalidate() | |
self.timer = nil | |
} | |
} | |
} | |
struct AudioChannelsLeftRight_Previews: PreviewProvider { | |
static var previews: some View { | |
Group { | |
AudioChannelsDemo() | |
.previewDisplayName(".leftToRight") | |
AudioChannelsDemo() | |
.environment(\.layoutDirection, .rightToLeft) | |
.previewDisplayName(".rightToLeft") | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment