Skip to content

Commit

Permalink
Minor Improvements (#13)
Browse files Browse the repository at this point in the history
* Added functionality to show user suggested grip types

* Fixed bug where sound cutoff in sound picker on iOS 17

* Minor info text changes

* Changed version number
  • Loading branch information
iDoc1 committed Jan 9, 2024
1 parent ff17a21 commit e045af0
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 61 deletions.
12 changes: 8 additions & 4 deletions CountDown.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
AB89A68B2B26562300BF1D80 /* glass_low.caf in Resources */ = {isa = PBXBuildFile; fileRef = AB89A6892B26562300BF1D80 /* glass_low.caf */; };
AB89A6932B265EF100BF1D80 /* WorkoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB89A6922B265EF100BF1D80 /* WorkoutTests.swift */; };
AB89A69C2B2668EA00BF1D80 /* GripsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB89A69B2B2668EA00BF1D80 /* GripsTest.swift */; };
AB8EB7212B4B9D3A00B5942E /* FormUtilitiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8EB7202B4B9D3A00B5942E /* FormUtilitiesTests.swift */; };
AB9DE2322B1BD98700CBBFD5 /* WorkoutHistoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB9DE2312B1BD98700CBBFD5 /* WorkoutHistoryDetailView.swift */; };
AB9DE2342B1BDD1C00CBBFD5 /* SectionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB9DE2332B1BDD1C00CBBFD5 /* SectionRow.swift */; };
AB9DE2362B1EDBB400CBBFD5 /* HistoryGripCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB9DE2352B1EDBB400CBBFD5 /* HistoryGripCardView.swift */; };
Expand Down Expand Up @@ -190,6 +191,7 @@
AB89A6902B265EF100BF1D80 /* CountDownUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CountDownUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
AB89A6922B265EF100BF1D80 /* WorkoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutTests.swift; sourceTree = "<group>"; };
AB89A69B2B2668EA00BF1D80 /* GripsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GripsTest.swift; sourceTree = "<group>"; };
AB8EB7202B4B9D3A00B5942E /* FormUtilitiesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormUtilitiesTests.swift; sourceTree = "<group>"; };
AB9DE2312B1BD98700CBBFD5 /* WorkoutHistoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutHistoryDetailView.swift; sourceTree = "<group>"; };
AB9DE2332B1BDD1C00CBBFD5 /* SectionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionRow.swift; sourceTree = "<group>"; };
AB9DE2352B1EDBB400CBBFD5 /* HistoryGripCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryGripCardView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -319,6 +321,7 @@
isa = PBXGroup;
children = (
AB7E61B82AC51DDD0081D475 /* TimerUtilitiesTests.swift */,
AB8EB7202B4B9D3A00B5942E /* FormUtilitiesTests.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -349,16 +352,16 @@
AB9DE22D2B1BD8B000CBBFD5 /* Grips */ = {
isa = PBXGroup;
children = (
ABD48EBD2B32A39300B14A51 /* BreakDurationPicker.swift */,
AB1BF8652AFDE74E004579FF /* GripEditView.swift */,
AB1581592AF9F4C900BCB088 /* GripCardView.swift */,
ABEB58C02AF48B25002C60F8 /* GripEditForm.swift */,
ABD48EBF2B32A5D400B14A51 /* GripTypePickers.swift */,
ABEB58C82AF700E4002C60F8 /* GripTypePickerView.swift */,
ABEB58C42AF49AB5002C60F8 /* NewGripView.swift */,
ABEB58CA2AF71541002C60F8 /* NewGripTypeView.swift */,
ABD48EB92B329D8900B14A51 /* RepDurationsPicker.swift */,
ABD48EBB2B32A19800B14A51 /* SetsRepsPickers.swift */,
ABD48EBD2B32A39300B14A51 /* BreakDurationPicker.swift */,
ABD48EBF2B32A5D400B14A51 /* GripTypePickers.swift */,
);
path = Grips;
sourceTree = "<group>";
Expand Down Expand Up @@ -803,6 +806,7 @@
ABF6A1B82AEACD7700822B7B /* ErrorMessagesTests.swift in Sources */,
AB9DE23A2B22E17C00CBBFD5 /* WorkoutHistoryPropertiesTest.swift in Sources */,
AB7E61B92AC51DDD0081D475 /* TimerUtilitiesTests.swift in Sources */,
AB8EB7212B4B9D3A00B5942E /* FormUtilitiesTests.swift in Sources */,
AB4E700A2B12771B00878AD7 /* GripPropertiesTests.swift in Sources */,
ABA68AF42AD4DF1500181690 /* TimerSoundTests.swift in Sources */,
AB3FD8362AC28BA5009D6C71 /* CountdownTimerTests.swift in Sources */,
Expand Down Expand Up @@ -1009,7 +1013,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.project.CountDown;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -1045,7 +1049,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.project.CountDown;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
18 changes: 18 additions & 0 deletions CountDown/Utilities/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,21 @@ import Foundation
// Global constants that are used througout the app
let maxNumberOfSets = 20
let maxNumberOfReps = 20

let defaultGripTypes = [
"Half Crimp",
"Open Hand Crimp",
"Full Crimp",
"Three Finger Drag",
"Jug",
"Warm Up Jug",
"Sloper",
"IM 2-Finger Pocket",
"MR 2-Finger Pocket",
"RP 2-Finger Pocket",
"IMR 3-Finger Pocket",
"MRP 3-Finger Pocket",
"Narrow Pinch",
"Medium Pinch",
"Wide Pinch"
].sorted()
14 changes: 14 additions & 0 deletions CountDown/Utilities/FormUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,17 @@ func workoutIsValidated(workout: WorkoutViewModel, errorMessages: ErrorMessages)

return isValidated
}



/// Returns the first 3 suggestions in the default list of grip types given a new grip type name. Used to offer grip types suggestions to the
/// user so they do not need to populate everyything themselves.
/// - Parameter newGripTypeName: The new grip type name
/// - Returns: A list of suggeste grip type strings
func getSuggestedGripTypes(newGripTypeName: String) -> [String] {
let newGripTypeName = newGripTypeName.trimmingCharacters(in: .whitespaces)
// Filter for first 3 default grip types that contain the new grip type text
return Array(defaultGripTypes.filter {
$0.lowercased().contains(newGripTypeName.lowercased())
}.prefix(3))
}
61 changes: 31 additions & 30 deletions CountDown/Views/Grips/GripTypePickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,46 @@ struct GripTypePickerView: View {
]) private var gripTypes: FetchedResults<GripType>

var body: some View {
Group {
if gripTypes.count == 0 {
List {
NewGripTypeView()
}
} else {
List {
Section {
ForEach(0..<gripTypes.count, id: \.self) { index in
HStack {
Button(action: {
selectedGripType = gripTypes[index]
}) {
HStack {
Text(gripTypes[index].unwrappedName)
.foregroundColor(colorScheme == .dark ? .white : .black)
Spacer()

if selectedGripType == gripTypes[index] {
Image(systemName: "checkmark")
.foregroundColor(.blue)
.accessibilityIdentifier("selectedGripType")
ScrollViewReader { proxy in
Group {
if gripTypes.count == 0 {
List {
NewGripTypeView(proxy: proxy)
}
} else {
List {
Section {
ForEach(0..<gripTypes.count, id: \.self) { index in
HStack {
Button(action: {
selectedGripType = gripTypes[index]
}) {
HStack {
Text(gripTypes[index].unwrappedName)
.foregroundColor(colorScheme == .dark ? .white : .black)
Spacer()

if selectedGripType == gripTypes[index] {
Image(systemName: "checkmark")
.foregroundColor(.blue)
.accessibilityIdentifier("selectedGripType")
}
}
}
}
}
.onDelete(perform: deleteGripTypes)
} footer: {
Text("Tap grip type to select it. Swipe left to delete it.")
}
.onDelete(perform: deleteGripTypes)
} footer: {
Text("Deleting a grip type currently in use by a grip will cause the grip" +
" type to show up as 'Grip Type Deleted'")
NewGripTypeView(proxy: proxy)
}
NewGripTypeView()
.id(UUID())
}
.id(UUID())
}
}
.navigationTitle("Grip Types")
.navigationTitle("Grip Types")
.navigationBarTitleDisplayMode(.inline)
}
}

/// Deletes the grip types at the given index values
Expand Down
80 changes: 56 additions & 24 deletions CountDown/Views/Grips/NewGripTypeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,77 @@

import SwiftUI

/// A view to add a new grip type
/// A view to add a new grip type. Accepts a ScrollViewProxy to enable this view to scroll to the bottom to show suggested grip types.
struct NewGripTypeView: View {
@Environment(\.managedObjectContext) private var moc
@State private var newGripTypeName = ""
@State private var suggestedGripTypes: [String] = []
let proxy: ScrollViewProxy

var body: some View {
Section {
HStack {
TextField("New Grip Type", text: $newGripTypeName.max(40))
Button(action: {
withAnimation {
let newGripType = GripType(context: moc)
newGripType.name = newGripTypeName

do {
try moc.save()
} catch {
print("Failed to save Grip Type: \(error)")
Group {
Section {
HStack {
TextField("New Grip Type", text: $newGripTypeName.max(40))
.id("textfield")
Button(action: {
withAnimation {
let newGripType = GripType(context: moc)
newGripType.name = newGripTypeName

do {
try moc.save()
} catch {
print("Failed to save Grip Type: \(error)")
}
newGripTypeName = ""
}
newGripTypeName = ""
}) {
Image(systemName: "plus.circle.fill")
.accessibilityLabel("Add Grip Type")
}
}) {
Image(systemName: "plus.circle.fill")
.accessibilityLabel("Add Grip Type")
.disabled(newGripTypeName.isEmpty)
}
} footer: {
Text("Add a new grip type to the list. Some examples are 'Half Crimp' " +
"and 'Three Finger Drag'.")
}

// Only show suggestions if grip type form is populated and at least one suggestions exists
if newGripTypeName.trimmingCharacters(in: .whitespaces).count > 0 &&
suggestedGripTypes.count > 0
{
Section {
ForEach(suggestedGripTypes, id: \.self) { gripTypeName in
Button(gripTypeName) {
newGripTypeName = gripTypeName
}
.id(gripTypeName)
}
} header: {
Text("Suggested Grip Types")
}
.disabled(newGripTypeName.isEmpty)
}
} footer: {
Text("Add new grip types here. Some examples are 'Half Crimp'," +
" 'Open Hand Crimp', or 'Three Finger Drag'.")
}
.onChange(of: newGripTypeName) { newValue in
withAnimation {
suggestedGripTypes = getSuggestedGripTypes(newGripTypeName: newValue)
}
}
// Scroll to bottom of suggestions list any time it changes
.onChange(of: suggestedGripTypes) { newValue in
proxy.scrollTo(suggestedGripTypes.last, anchor: .bottom)
}
}
}

struct NewGripTypeView_Previews: PreviewProvider {
static var previews: some View {
List {
NewGripTypeView()
.previewLayout(.fixed(width: 400, height: 60))
ScrollViewReader { proxy in
List {
NewGripTypeView(proxy: proxy)
.previewLayout(.fixed(width: 400, height: 60))
}
}
}
}
2 changes: 1 addition & 1 deletion CountDown/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ struct SettingsView: View {
} header: {
Text("Timer Feedback")
} footer: {
Text("Silent mode must be disabled for sound to function")
Text("Silent mode must be disabled for sound to work")
}

Section {
Expand Down
5 changes: 3 additions & 2 deletions CountDown/Views/SoundPickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ struct SoundPickerView: View {
@Environment(\.colorScheme) private var colorScheme
@Binding var selectedSound: TimerSound
private let soundTypes = TimerSound.allCases.map { $0 }
private let players = TimerSound.allCases.map { TimerSoundPlayer(type: $0) }
// Must be static otherwise UI refresh will cause sound to be cutoff
private static let players = TimerSound.allCases.map { TimerSoundPlayer(type: $0) }

var body: some View {
List {
Expand Down Expand Up @@ -45,7 +46,7 @@ struct SoundPickerView: View {
/// Plays the sound corresponding to the given index in the players array
/// - Parameter index: The index at which to play the sound
private func playSoundAtIndex(_ index: Int) {
players[index].playLowSound()
SoundPickerView.players[index].playLowSound()
}
}

Expand Down
27 changes: 27 additions & 0 deletions CountDownTests/Utilities/FormUtilitiesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// FormUtilitiesTests.swift
// CountDownTests
//
// Created by Ian Docherty on 1/7/24.
//

import XCTest
@testable import CountDown

final class FormUtilitiesTests: XCTestCase {

func testSuggestedGripTypesAreCorrect() throws {
let suggested = getSuggestedGripTypes(newGripTypeName: "cRi")
XCTAssertEqual(suggested, ["Full Crimp", "Half Crimp", "Open Hand Crimp"])
}

func testSuggestedGripTypesIsEmpty() {
let suggested = getSuggestedGripTypes(newGripTypeName: "CRIMPX")
XCTAssertEqual(suggested, [])
}

func testSuggesteGripTypesWorksWithSpaces() {
let suggested = getSuggestedGripTypes(newGripTypeName: " crim")
XCTAssertEqual(suggested, ["Full Crimp", "Half Crimp", "Open Hand Crimp"])
}
}

0 comments on commit e045af0

Please sign in to comment.