Skip to content

Commit

Permalink
refactor: Decouple settings vm from user defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
onato committed Apr 26, 2024
1 parent 664bfdc commit 3855fcc
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 50 deletions.
20 changes: 20 additions & 0 deletions TypeReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
A792D69E2BCE30FE00D3ABDB /* SpeechSettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A792D69D2BCE30FE00D3ABDB /* SpeechSettingsViewModelTests.swift */; };
A792D6A02BCF5C8E00D3ABDB /* PDFViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A792D69F2BCF5C8E00D3ABDB /* PDFViewer.swift */; };
A792D6DB2BD5C20200D3ABDB /* PDFViewerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A792D6DA2BD5C20200D3ABDB /* PDFViewerTests.swift */; };
A79FA4302BDB1BC900CF949D /* AppSettingsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79FA42F2BDB1BC900CF949D /* AppSettingsMock.swift */; };
A79FA4322BDB1F8700CF949D /* UserDefaultsAppSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79FA4312BDB1F8700CF949D /* UserDefaultsAppSettingsTests.swift */; };
A79FA4352BDB253700CF949D /* UserDefaultsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79FA4342BDB253700CF949D /* UserDefaultsSettings.swift */; };
A7DF8BD92BD5CECA0061A2B5 /* PDFHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DF8BD82BD5CECA0061A2B5 /* PDFHighlighter.swift */; };
A7F073DA2BD5E7A2002E06B3 /* SentenceFindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F073D92BD5E7A2002E06B3 /* SentenceFindingTests.swift */; };
A7F073DD2BD5FA63002E06B3 /* String+SentenceFinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F073DC2BD5FA63002E06B3 /* String+SentenceFinding.swift */; };
Expand Down Expand Up @@ -62,6 +65,9 @@
A792D69D2BCE30FE00D3ABDB /* SpeechSettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechSettingsViewModelTests.swift; sourceTree = "<group>"; };
A792D69F2BCF5C8E00D3ABDB /* PDFViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFViewer.swift; sourceTree = "<group>"; };
A792D6DA2BD5C20200D3ABDB /* PDFViewerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFViewerTests.swift; sourceTree = "<group>"; };
A79FA42F2BDB1BC900CF949D /* AppSettingsMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsMock.swift; sourceTree = "<group>"; };
A79FA4312BDB1F8700CF949D /* UserDefaultsAppSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsAppSettingsTests.swift; sourceTree = "<group>"; };
A79FA4342BDB253700CF949D /* UserDefaultsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsSettings.swift; sourceTree = "<group>"; };
A7DF8BD82BD5CECA0061A2B5 /* PDFHighlighter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFHighlighter.swift; sourceTree = "<group>"; };
A7F073D92BD5E7A2002E06B3 /* SentenceFindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentenceFindingTests.swift; sourceTree = "<group>"; };
A7F073DC2BD5FA63002E06B3 /* String+SentenceFinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SentenceFinding.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -108,6 +114,7 @@
A78F726C2BC3827C000919DE /* TypeReader */ = {
isa = PBXGroup;
children = (
A79FA4332BDB252700CF949D /* Settings */,
A7F073DB2BD5FA46002E06B3 /* StringParsing */,
A7DF8BD72BD5CEAD0061A2B5 /* PDF */,
A78F72AA2BC4BC7F000919DE /* Views */,
Expand Down Expand Up @@ -141,6 +148,7 @@
A792D69D2BCE30FE00D3ABDB /* SpeechSettingsViewModelTests.swift */,
A792D6DA2BD5C20200D3ABDB /* PDFViewerTests.swift */,
A7F073D92BD5E7A2002E06B3 /* SentenceFindingTests.swift */,
A79FA4312BDB1F8700CF949D /* UserDefaultsAppSettingsTests.swift */,
);
path = TypeReaderTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -185,10 +193,19 @@
isa = PBXGroup;
children = (
A792D69B2BCE2B7800D3ABDB /* UserSettingsMock.swift */,
A79FA42F2BDB1BC900CF949D /* AppSettingsMock.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
A79FA4332BDB252700CF949D /* Settings */ = {
isa = PBXGroup;
children = (
A79FA4342BDB253700CF949D /* UserDefaultsSettings.swift */,
);
path = Settings;
sourceTree = "<group>";
};
A7DF8BD72BD5CEAD0061A2B5 /* PDF */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -316,6 +333,7 @@
A792D6A02BCF5C8E00D3ABDB /* PDFViewer.swift in Sources */,
A78F72A52BC39B12000919DE /* SpeechSynthesizer.swift in Sources */,
A78F72A92BC4BC65000919DE /* SpeechSettingsViewModel.swift in Sources */,
A79FA4352BDB253700CF949D /* UserDefaultsSettings.swift in Sources */,
A7F073DF2BD74F52002E06B3 /* ErrorMessage.swift in Sources */,
A78F729A2BC39267000919DE /* PDFTextExtractor.swift in Sources */,
A78F72982BC3840E000919DE /* DocumentPicker.swift in Sources */,
Expand All @@ -336,7 +354,9 @@
A792D6DB2BD5C20200D3ABDB /* PDFViewerTests.swift in Sources */,
A792D69E2BCE30FE00D3ABDB /* SpeechSettingsViewModelTests.swift in Sources */,
A792D69C2BCE2B7800D3ABDB /* UserSettingsMock.swift in Sources */,
A79FA4322BDB1F8700CF949D /* UserDefaultsAppSettingsTests.swift in Sources */,
A78F729C2BC393B7000919DE /* PDFTextExtractorTests.swift in Sources */,
A79FA4302BDB1BC900CF949D /* AppSettingsMock.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
36 changes: 36 additions & 0 deletions TypeReader/Settings/UserDefaultsSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import AVFoundation

protocol AppSettings {
var selectedVoiceIdentifier: String { get set }
var speechRate: Float { get set }
}

class UserDefaultsSettings: AppSettings {
private let userSettings: UserSettings

var selectedVoiceIdentifier: String {
didSet {
userSettings.set(selectedVoiceIdentifier, forKey: "selectedVoiceIdentifier")
}
}
var speechRate: Float {
didSet {
userSettings.set(speechRate, forKey: "speechRate")
}
}
init(userDefaults: UserSettings = UserDefaults.standard) {
self.userSettings = userDefaults
if let voiceIdentifier = userSettings.string(forKey: "selectedVoiceIdentifier") {
selectedVoiceIdentifier = voiceIdentifier
} else {
selectedVoiceIdentifier = (AVSpeechSynthesisVoice.speechVoices().first { voice in
voice.language == AVSpeechSynthesisVoice.currentLanguageCode()
})?.identifier ?? "There are no voices installed"
}
self.speechRate = userSettings.float(forKey: "speechRate")
if self.speechRate == 0 {
self.speechRate = AVSpeechUtteranceDefaultSpeechRate
}
}
}
29 changes: 6 additions & 23 deletions TypeReader/ViewModels/SpeechSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,15 @@ import Foundation
import AVFoundation

class SpeechSettingsViewModel: ObservableObject {
private let userSettings: UserSettings
@Published var selectedVoiceIdentifier: String

@Published var selectedVoiceIdentifier: String {
didSet {
userSettings.set(selectedVoiceIdentifier, forKey: "selectedVoiceIdentifier")
}
}

@Published var speechRate: Float {
didSet {
userSettings.set(speechRate, forKey: "speechRate")
}
}
@Published var speechRate: Float

var voices: [AVSpeechSynthesisVoice] = []

init(userDefaults: UserSettings = UserDefaults.standard) {
self.userSettings = userDefaults
// Load saved settings or defaults
self.selectedVoiceIdentifier = userSettings.string(forKey: "selectedVoiceIdentifier") ?? AVSpeechSynthesisVoice.currentLanguageCode()
self.speechRate = userSettings.float(forKey: "speechRate")
if self.speechRate == 0 {
self.speechRate = AVSpeechUtteranceDefaultSpeechRate
}

// Fetch available voices
self.voices = AVSpeechSynthesisVoice.speechVoices()
init(settings: AppSettings = UserDefaultsSettings()) {
selectedVoiceIdentifier = settings.selectedVoiceIdentifier
speechRate = settings.speechRate
voices = AVSpeechSynthesisVoice.speechVoices()
}
}
23 changes: 23 additions & 0 deletions TypeReaderTests/Mocks/AppSettingsMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation
@testable import TypeReader

// MARK: - AppSettingsMock -

final class AppSettingsMock: AppSettings {

// MARK: - selectedVoiceIdentifier

var selectedVoiceIdentifier: String {
get { underlyingSelectedVoiceIdentifier }
set(value) { underlyingSelectedVoiceIdentifier = value }
}
internal var underlyingSelectedVoiceIdentifier: String!

// MARK: - speechRate

var speechRate: Float {
get { underlyingSpeechRate }
set(value) { underlyingSpeechRate = value }
}
internal var underlyingSpeechRate: Float!
}
32 changes: 5 additions & 27 deletions TypeReaderTests/SpeechSettingsViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,13 @@ import Nimble

final class SpeechSettingsViewModelTests: XCTestCase {
func test_SettingsViewModel_whenSettings_shouldSetRate() throws {
let mockUserSettings = UserSettingsMock()
mockUserSettings.floatForKeyReturnValue = 1.5
mockUserSettings.stringForKeyReturnValue = "com.apple.speech.synthesis.voice.Princess"

let sut = SpeechSettingsViewModel(userDefaults: mockUserSettings)
let mockSettings = AppSettingsMock()
mockSettings.speechRate = 1.5
mockSettings.selectedVoiceIdentifier = "com.apple.speech.synthesis.voice.Princess"

let sut = SpeechSettingsViewModel(settings: mockSettings)

expect(sut.speechRate) == 1.5
expect(sut.selectedVoiceIdentifier) == "com.apple.speech.synthesis.voice.Princess"
}

func test_SettingsViewModel_whenNoSettings_shouldSetRate() throws {
let mockUserSettings = UserSettingsMock()
mockUserSettings.floatForKeyReturnValue = 0

let sut = SpeechSettingsViewModel(userDefaults: mockUserSettings)

expect(sut.speechRate) == 0.5
}

func test_SettingsViewModel_whenSetID_shouldSaveID() throws {
let mockUserSettings = UserSettingsMock()
mockUserSettings.floatForKeyReturnValue = 0

let sut = SpeechSettingsViewModel(userDefaults: mockUserSettings)
sut.selectedVoiceIdentifier = "com.apple.speech.synthesis.voice.Princess"
sut.speechRate = 0.6

let identifier = try XCTUnwrap(mockUserSettings.setForKeyReceivedArguments?.value as? String)
expect(identifier) == "com.apple.speech.synthesis.voice.Princess"
expect(mockUserSettings.setFloatForKeyReceivedArguments?.value) == 0.6
}
}
26 changes: 26 additions & 0 deletions TypeReaderTests/UserDefaultsAppSettingsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@testable import TypeReader
import XCTest
import Nimble

final class UserDefaultsAppSettingsTests: XCTestCase {
func test_SettingsViewModel_whenSettings_shouldSetDefaults() throws {
let defaults = UserDefaults()
let sut = UserDefaultsSettings(userDefaults: defaults)

sut.speechRate = 1.5
sut.selectedVoiceIdentifier = "com.apple.speech.synthesis.voice.Princess"

expect(defaults.string(forKey: "selectedVoiceIdentifier")) == "com.apple.speech.synthesis.voice.Princess"
expect(defaults.float(forKey: "speechRate")) == 1.5
}

func test_SettingsViewModel_whenNoDefaults_shouldSetRate() throws {
let mockUserSettings = UserSettingsMock()
mockUserSettings.floatForKeyReturnValue = 0

let sut = UserDefaultsSettings(userDefaults: mockUserSettings)

expect(sut.speechRate) == 0.5
expect(sut.selectedVoiceIdentifier) != nil
}
}

0 comments on commit 3855fcc

Please sign in to comment.