Skip to content

Instantly share code, notes, and snippets.

@mattyoung
Last active May 16, 2020 18:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattyoung/c26f1daa637732801a0b26cd0e510b8f to your computer and use it in GitHub Desktop.
Save mattyoung/c26f1daa637732801a0b26cd0e510b8f to your computer and use it in GitHub Desktop.
/*
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