Skip to content

Commit

Permalink
Ios mute fix after discussion (#296)
Browse files Browse the repository at this point in the history
* ios mute button

* adjustment made based on dicsussion for Ios Mute button

* fixed the in the file

* changed name of file so the names are consistent

* adjusted based on the other buttons

* Clean up and fixes to get this ready

* Use a boring ObservableObject and use UDF

* Delete .gitmodules

* Delete extraneous submodule

* Try to fix CI

* Attempt to improve UI responsiveness

* Improve speed limit display

* Hide when not navigating

* Add missing docs

* Swiftformat

* Update snapshots

---------

Co-authored-by: Marek Sabol <mareksabol@MacBook-Pro-uzivatela-Marek.local>
Co-authored-by: Ian Wagner <ian.wagner@stadiamaps.com>
Co-authored-by: Jacob Fielding <jf0517@gmail.com>
  • Loading branch information
4 people authored Oct 29, 2024
1 parent 14b41be commit dbee23a
Show file tree
Hide file tree
Showing 35 changed files with 432 additions and 79 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ jobs:
fetch-depth: 0 # Ensure that we can operate on the full history
ref: main

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.0'

- name: Build iOS XCFramework
run: ./build-ios.sh --release
working-directory: common
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ jobs:
with:
file_pattern: 'Package.swift'

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.0'

- name: Build iOS XCFramework
run: ./build-ios.sh --release
working-directory: common
Expand Down Expand Up @@ -82,7 +86,7 @@ jobs:
- name: Configure Package.swift for local development
run: sed -i '' 's/let useLocalFramework = false/let useLocalFramework = true/' Package.swift

- name: Download libferrostar-rs.xcframework.
- name: Download libferrostar-rs.xcframework
uses: actions/download-artifact@v4
with:
path: common
Expand All @@ -94,7 +98,7 @@ jobs:

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.3'
xcode-version: '16.0'

- name: Install xcbeautify
run: brew install xcbeautify
Expand Down Expand Up @@ -149,7 +153,7 @@ jobs:

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.3'
xcode-version: '16.0'

- name: Install xcbeautify
run: brew install xcbeautify
Expand Down
Empty file removed .gitmodules
Empty file.
4 changes: 3 additions & 1 deletion apple/DemoApp/Demo/DemoNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct DemoNavigationView: View {
private let navigationDelegate = NavigationDelegate()
// NOTE: This is probably not ideal but works for demo purposes.
// This causes a thread performance checker warning log.
private let spokenInstructionObserver = AVSpeechSpokenInstructionObserver(isMuted: false)
private let spokenInstructionObserver = SpokenInstructionObserver.initAVSpeechSynthesizer(isMuted: false)

private var locationProvider: LocationProviding
@ObservedObject private var ferrostarCore: FerrostarCore
Expand Down Expand Up @@ -79,6 +79,8 @@ struct DemoNavigationView: View {
styleURL: style,
camera: $camera,
navigationState: ferrostarCore.state,
isMuted: spokenInstructionObserver.isMuted,
onTapMute: spokenInstructionObserver.toggleMute,
onTapExit: { stopNavigation() },
makeMapContent: {
let source = ShapeSource(identifier: "userLocation") {
Expand Down
9 changes: 9 additions & 0 deletions apple/Sources/FerrostarCore/NavigationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,13 @@ public struct NavigationState: Hashable {

return annotationJson
}

public var isNavigating: Bool {
switch tripState {
case .navigating:
true
case .complete, .idle:
false
}
}
}
54 changes: 0 additions & 54 deletions apple/Sources/FerrostarCore/Speech.swift

This file was deleted.

33 changes: 33 additions & 0 deletions apple/Sources/FerrostarCore/Speech/SpeechSynthesizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import AVFoundation
import Foundation

/// An abstracted speech synthesizer that is used by the ``SpokenInstructionObserver``
///
/// Any functions that are needed for use in the ``SpokenInstructionObserver`` should be exposed through
/// this protocol.
public protocol SpeechSynthesizer {
// TODO: We could further abstract this to allow other speech synths.
// E.g. with a `struct SpeechUtterance` if and when another speech service comes along.

var isSpeaking: Bool { get }
func speak(_ utterance: AVSpeechUtterance)
@discardableResult
func stopSpeaking(at boundary: AVSpeechBoundary) -> Bool
}

extension AVSpeechSynthesizer: SpeechSynthesizer {
// No def required
}

class PreviewSpeechSynthesizer: SpeechSynthesizer {
public var isSpeaking: Bool = false

public func speak(_: AVSpeechUtterance) {
// No action for previews
}

public func stopSpeaking(at _: AVSpeechBoundary) -> Bool {
// No action for previews
true
}
}
76 changes: 76 additions & 0 deletions apple/Sources/FerrostarCore/Speech/SpokenInstructionObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import AVFoundation
import Combine
import FerrostarCoreFFI
import Foundation

/// An Spoken instruction provider that takes a speech synthesizer.
public class SpokenInstructionObserver: ObservableObject {
@Published public private(set) var isMuted: Bool

private let synthesizer: SpeechSynthesizer
private let queue = DispatchQueue(label: "ferrostar-spoken-instruction-observer", qos: .default)

/// Create a spoken instruction observer with any ``SpeechSynthesizer``
///
/// - Parameters:
/// - synthesizer: The speech synthesizer.
/// - isMuted: Whether the speech synthesizer is currently muted. Assume false if unknown.
public init(
synthesizer: SpeechSynthesizer,
isMuted: Bool
) {
self.synthesizer = synthesizer
self.isMuted = isMuted
}

public func spokenInstructionTriggered(_ instruction: FerrostarCoreFFI.SpokenInstruction) {
guard !isMuted else {
return
}

let utterance: AVSpeechUtterance = if #available(iOS 16.0, *),
let ssml = instruction.ssml,
let ssmlUtterance = AVSpeechUtterance(ssmlRepresentation: ssml)
{
ssmlUtterance
} else {
AVSpeechUtterance(string: instruction.text)
}

queue.async {
self.synthesizer.speak(utterance)

Check warning on line 41 in apple/Sources/FerrostarCore/Speech/SpokenInstructionObserver.swift

View workflow job for this annotation

GitHub Actions / test (FerrostarCore-Package, platform=iOS Simulator,name=iPhone 15,OS=17.2)

capture of 'utterance' with non-sendable type 'AVSpeechUtterance' in a `@Sendable` closure; this is an error in the Swift 6 language mode
}
}

/// Toggle the mute.
public func toggleMute() {
let isCurrentlyMuted = isMuted
isMuted = !isCurrentlyMuted

// This used to have `synthesizer.isSpeaking`, but I think we want to run it regardless.
if isMuted {
queue.async {
self.stopAndClearQueue()
}
}
}

public func stopAndClearQueue() {
synthesizer.stopSpeaking(at: .immediate)
}
}

public extension SpokenInstructionObserver {
/// Create a new spoken instruction observer with AFFoundation's AVSpeechSynthesizer.
///
/// - Parameters:
/// - synthesizer: An instance of AVSpeechSynthesizer. One is provided by default, but you can inject your own.
/// - isMuted: If the synthesizer is muted. This should be false unless you're providing a "hot" synth that is
/// speaking.
/// - Returns: The instance of SpokenInstructionObserver
static func initAVSpeechSynthesizer(synthesizer: AVSpeechSynthesizer = AVSpeechSynthesizer(),
isMuted: Bool = false) -> SpokenInstructionObserver
{
SpokenInstructionObserver(synthesizer: synthesizer, isMuted: isMuted)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?

let isMuted: Bool
let onTapMute: () -> Void
var onTapExit: (() -> Void)?

public var minimumSafeAreaInsets: EdgeInsets
Expand All @@ -49,13 +51,17 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
isMuted: Bool,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapMute: @escaping () -> Void,
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.isMuted = isMuted
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapMute = onTapMute
self.onTapExit = onTapExit

userLayers = makeMapContent()
Expand Down Expand Up @@ -88,6 +94,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
navigationState: navigationState,
speedLimit: speedLimit,
speedLimitStyle: speedLimitStyle,
isMuted: isMuted,
showMute: navigationState?.isNavigating == true,
onMute: onTapMute,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
Expand All @@ -109,6 +118,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
navigationState: navigationState,
speedLimit: speedLimit,
speedLimitStyle: speedLimitStyle,
isMuted: isMuted,
showMute: navigationState?.isNavigating == true,
onMute: onTapMute,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
Expand Down Expand Up @@ -151,7 +163,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
return DynamicallyOrientingNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Expand All @@ -170,7 +184,9 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
return DynamicallyOrientingNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
public var midLeading: (() -> AnyView)?
public var bottomTrailing: (() -> AnyView)?

let isMuted: Bool
let onTapMute: () -> Void
var onTapExit: (() -> Void)?

public var minimumSafeAreaInsets: EdgeInsets
Expand All @@ -49,13 +51,17 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
camera: Binding<MapViewCamera>,
navigationCamera: MapViewCamera = .automotiveNavigation(),
navigationState: NavigationState?,
isMuted: Bool,
minimumSafeAreaInsets: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
onTapMute: @escaping () -> Void,
onTapExit: (() -> Void)? = nil,
@MapViewContentBuilder makeMapContent: () -> [StyleLayerDefinition] = { [] }
) {
self.styleURL = styleURL
self.navigationState = navigationState
self.isMuted = isMuted
self.minimumSafeAreaInsets = minimumSafeAreaInsets
self.onTapMute = onTapMute
self.onTapExit = onTapExit

userLayers = makeMapContent()
Expand All @@ -82,6 +88,9 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
navigationState: navigationState,
speedLimit: speedLimit,
speedLimitStyle: speedLimitStyle,
isMuted: isMuted,
showMute: true,
onMute: onTapMute,
showZoom: true,
onZoomIn: { camera.incrementZoom(by: 1) },
onZoomOut: { camera.incrementZoom(by: -1) },
Expand Down Expand Up @@ -119,7 +128,9 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
return LandscapeNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Expand All @@ -140,7 +151,9 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
return LandscapeNavigationView(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!,
camera: .constant(.center(userLocation.clLocation.coordinate, zoom: 12)),
navigationState: state
navigationState: state,
isMuted: true,
onTapMute: {}
)
.navigationFormatterCollection(FoundationFormatterCollection(distanceFormatter: formatter))
}
Loading

0 comments on commit dbee23a

Please sign in to comment.