Observing system volume in SwiftUI - ios

I am trying to show a volume indicator in my app, but first I need to monitor the systems current volume.
I am using an observer, and while the print statement shows the correct value, the UI never does.
import SwiftUI
import MediaPlayer
struct ContentView: View {
#State var vol: Float = 1.0
// Audio session object
private let session = AVAudioSession.sharedInstance()
// Observer
private var progressObserver: NSKeyValueObservation!
init() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient)
try session.setActive(true, options: .notifyOthersOnDeactivation)
self.vol = 1.0
} catch {
print("cannot activate session")
}
progressObserver = session.observe(\.outputVolume) { [self] (session, value) in
print(session.outputVolume)
self.vol = session.outputVolume
}
}
var body: some View {
Text(String(self.vol))
}
}
// fixed (set category to ambient)(updated above code)
Also, every time the application is launched, it stops all currently playing music.

Solved. Created a class that conforms to ObservableObject and use the ObservedObject property in the view. Also, the volume observer doesn't work in the simulator, only on device.
VolumeObserver.swift
import Foundation
import MediaPlayer
final class VolumeObserver: ObservableObject {
#Published var volume: Float = AVAudioSession.sharedInstance().outputVolume
// Audio session object
private let session = AVAudioSession.sharedInstance()
// Observer
private var progressObserver: NSKeyValueObservation!
func subscribe() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient)
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("cannot activate session")
}
progressObserver = session.observe(\.outputVolume) { [self] (session, value) in
DispatchQueue.main.async {
self.volume = session.outputVolume
}
}
}
func unsubscribe() {
self.progressObserver.invalidate()
}
init() {
subscribe()
}
}
ContentView.swift
import SwiftUI
import MediaPlayer
struct ContentView: View {
#ObservedObject private var volObserver = VolumeObserver()
init() {
print(vol.volume)
}
var body: some View {
Text(String(vol.volume))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

I want to change ContentViews environment object based on Condition

import SwiftUI
import Firebase
#main
struct TSUDateApp: App {
#AppStorage(CurrentUserDefaults.userID) var currentUserID: String?
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
if currentUserID != nil {
ContentView().environmentObject(AuthViewModel())
.environmentObject(MessageListVM(userID: currentUserID!))
} else {
ContentView().environmentObject(AuthViewModel())
}
}
}
}
I want to change ContentViews environment object based on Condition, but it iterates only once and it is not updated if currentUserIDchanges, if i want to get logic I want i have to reload app than it is working. I tried .onReceive and .onChange, but it always fails.
I am changin currentUserID in signOut() function here
func signOut() {
navigateToLoginView()
let defaultsDictionary = UserDefaults.standard.dictionaryRepresentation()
defaultsDictionary.keys.forEach { key in
UserDefaults.standard.removeObject(forKey: key)
}
try? Auth.auth().signOut()
}
The issue is that #AppStorage does not refresh your view if something changes UserDefaults through any other method or even another AppStorage with the same key.
The most simple solution in this case would be to move the currentUserID to one of the ViewModels. As you are using AuthViewModel for both cases it seems that this is the appropriate container for this.
class AuthViewModel: ObservableObject{
#AppStorage("currentUserID") var currentUserID: String?
}
struct TSUDateApp: App {
#StateObject private var authViewModel = AuthViewModel()
var body: some Scene {
WindowGroup {
if let currentUserID = authViewModel.currentUserID{
ContentView()
.environmentObject(authViewModel)
.environmentObject(MessageListVM(userID: currentUserID))
} else {
ContentView().environmentObject(authViewModel)
}
}
}
}
and yoursignOut function would look like this:
func signOut() {
navigateToLoginView()
authViewModel.currentUserId = nil
try? Auth.auth().signOut()
}
of course you would need to pull it from the environment first:
#EnvironmentObject private var authViewModel: AuthViewModel
Another approach would be to pass your currentUserID on as a Binding to your ContentView:
struct TSUDateApp: App {
#AppStorage("currentUserID") var currentUserID: String?
var body: some View{
if let currentUserID = currentUserID{
ContentView(currentUserID: $currentUserID)
.environmentObject(AuthViewModel())
.environmentObject(MessageListVM(userID: currentUserID))
} else {
ContentView(currentUserID: $currentUserID).environmentObject(AuthViewModel())
}
}
}
In ContentView create the Binding and manipulate it in signOut.
#Binding var currentUserID: String?
func signOut() {
navigateToLoginView()
currentUserId = nil
try? Auth.auth().signOut()
}

How to control AVPlayer playback in SwiftUI

I am trying to play music on my app and manage to play/stop the music from the app's settings.
First I am creating an ObservableObject class called MusicPlayer:
class MusicPlayer: ObservableObject {
#Published var isPlaying = AppDefaults.shared.isMusicPlaying()
#Published var music : AVAudioPlayer? = nil
func playMusic() {
guard let strFilePath = Bundle.main.path(forResource: "music", ofType: "mp3") else { return }
do {
music = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: strFilePath))
} catch {
print(error)
}
music?.volume = 0.60
music?.numberOfLoops = -1
if isPlaying {
music?.play()
} else {
music?.stop()
}
}
}
and then play the music in main app file:
#main
struct AppName: App {
#StateObject private var player = MusicPlayer()
var body: some Scene {
WindowGroup {
ContentsView()
.onAppear {
player.playMusic()
}
}
}
}
and then trying to stop/play the music using toggle from settings:
struct SettingsView: View {
#StateObject private var player = MusicPlayer()
var body: some View {
Toggle("Music", isOn: $player.isPlaying)
.onChange(of: player.isPlaying, perform: { _ in
AppDefaults.shared.setMusic(player.isPlaying)
if player.isPlaying {
player.music?.stop()
} else {
player.music?.play()
}
})
}
}
now the problem is switching to on or off doesn't change the state of playing. How can I fix this issue?
The issue here is you are initializing your Viewmodel twice. So you have 2 different sources of truth. So there are 2 different AVAudioPlayer.
Solution: Create one single instance in the top View and pass this on to the views that need this.
As you decided to omit how SettingsView correlate with the other Views I can only give a more general solution.
Let´s asume the SettingsView is used in AppName:
#StateObject private var player = MusicPlayer()
WindowGroup {
....(ContentView stuff)
SettingsView()
// pass the observableObject on to the SettingsView and its children
.environmentObject(player)
}
Then in SettingsView:
struct SettingsView: View {
// get the observableObject from the environment
#EnvironmentObject private var player: MusicPlayer
var body: some View {
Toggle("Music", isOn: $player.isPlaying)
.onChange(of: player.isPlaying, perform: { _ in
AppDefaults.shared.setMusic(player.isPlaying)
if player.isPlaying {
player.music?.stop()
} else {
player.music?.play()
}
})
}
}

Issue passing data from API call in SwiftUI MVVM pattern

been going back and forth for 2 days trying to figure this out before posting and still hitting a wall.
Created an API specific class, a ViewModel, and a View and trying to shuttle data back and forth and while I see the API call is successful and I decode it without issue on logs, it never reflects on the UI or View.
As far as I see I appear to be trying to access the data before it's actually available. All help greatly appreciated!
API Class:
import Combine
import Foundation
class CrunchbaseApi:ObservableObject
{
#Published var companies:[Company] = [Company]()
#Published var singleCompany:Company?
func retrieve(company:String) async
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
func retrieveCompanyList()
{
//declare
}
}
ViewModel:
import Combine
import Foundation
class CompanyViewModel: ObservableObject
{
var crunchbase:CrunchbaseApi = CrunchbaseApi()
#Published var singleCompany:Company?
func retrieveCompany(company:String) async
{
await self.crunchbase.retrieve(company: company)
self.singleCompany = crunchbase.singleCompany
}
}
View:
import SwiftUI
struct CompanyView: View
{
#State var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
}
}
}
Your assumption about accessing the data to early is correct. But there are more things going on here.
just declaring a function async like your retrieve func doesn´t make it async.
using a nested Observable class with #Published will not update the view
Observable classes should have either an #StateObject or an #ObservableObject property wrapper. Depending on if the class is injected or created in the view
Possible solution:
Move the function into the viewmodel:
class CompanyViewModel: ObservableObject
{
#Published var singleCompany:Company?
func retrieve(company:String)
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
Change the View to hold the viewmodel as #StateObject, also add an .onApear modifier to load the data:
struct CompanyView: View
{
#StateObject var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
.onAppear {
companyViewModel.retrieve(company: "whatever")
}
}
}
}

SwiftUI: How can I catch changing value from observed object when I execute function

I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...

Pass EnvironmentObject to an ObservableObject class [duplicate]

This question already has an answer here:
Swiftui - How do I initialize an observedObject using an environmentobject as a parameter?
(1 answer)
Closed 2 years ago.
I have made a SwiftUI app that repeatedly fetches telemetry data to update custom views. The views use a variable stored in an EnvironmentObject.
struct updateEO{
#EnvironmentObject var settings:UserSettings
func pushSettingUpdate(telemetry: TelemetryData) {
settings.info = telemetry
print(settings.info)
}
}
class DownloadTimer : ObservableObject {
var timer : Timer!
let didChange = PassthroughSubject<DownloadTimer,Never>()
#Published var telemetry = TelemetryData()
func start() {
connectToClient()
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {
_ in
guard let url = URL(string: "http://127.0.0.1:25555/api/telemetry") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(TelemetryData.self, from: data) {
DispatchQueue.main.async {
updateEO().pushSettingUpdate(telemetry: decodedResponse)
}
return
}
}
}.resume()
}
}
}
At runtime, when the telemetry is passed to the pushSettingUpdate(telemetry: decodedResponse), the app crashes with an error of 'Fatal error: No ObservableObject of type UserSettings found.'.
I understand I may need to pass the struct the EnvironmentObject but I am not sure on how to do that. Any help would be much appreciated. Thanks! :)
You should use #EnvironmentObject in your view and pass it down to your model if needed.
Here, struct updateEO is not a view.
I've created a simpler example to show you how to do this :
UserSettings
class UserSettings: ObservableObject {
#Published var info: String = ""
}
DownloadTimer
class DownloadTimer: ObservableObject {
var timer : Timer?
func start(settings: UserSettings) {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { t in
settings.info = t.fireDate.description
}
}
}
And you call start (with UserSettings as parameter) when the Text appears.
MyView
struct MyView: View {
#StateObject private let downloadTimer = DownloadTimer()
#EnvironmentObject var settings: UserSettings
var body: some View {
Text(settings.info)
.onAppear {
self.downloadTimer.start(settings: self.settings)
}
}
}
And don't forget to call .environmentObject function to inject your UserSettings in SceneDelegate.
SceneDelegate
let contentView = MyView().environmentObject(UserSettings())
You should see the text updating as time goes by.

Resources