I have a strange problem with the SwiftUI Alert view. In an ObservableObject, I do some network requests and in case of a error I will show a alert. This is my simplified model:
class MyModel: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var isError: Bool = false
public func network() {
Service.call() {
self.isError = true
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
}
Service.call is a dummy for my network request. My view looks like:
struct MyView: View {
#ObservedObject var model: MyModel
var body: some View {
…
.alert(isPresented: self.$model.isError) {
print("Error Alert")
return Alert(title: Text("Alert"))
}
}
}
On the first call, everything works and the alert is shown. For all further calls,print("Error Alert") will be executed and Error Alert appears in the console, but the alert is not shown.
Does anyone have any idea why Alert is only shown once?
Try to use instead (there is already default publisher for #Published properties)
class MyModel: ObservableObject {
#Published var isError: Bool = false
public func network() {
Service.call() {
DispatchQueue.main.async {
self.isError = true // << !!! important place to call
}
}
}
}
Related
I want to output the variable 'healthStore.valueTest' via ContentView in SwiftUI.
The class healtStore is structured as follows:
class HealthStore {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
// let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())
// let anchorDate = Date.mondayAt12AM()
// let daily = DateComponents(day: 1)
// let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
ContentView is built as follows:
import SwiftUI
import HealthKit
struct ContentView: View {
private var healthStore: HealthStore?
init() {
healthStore = HealthStore()
}
var body: some View {
Text("Hello, world!")
.padding().onAppear(){
if let healthStore = healthStore {
healthStore.requestAuthorization { success in
if success {
healthStore.calculateBloodPressureSystolic()
print(healthStore.query)
print(healthStore.valueTest)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The value for the variable self.valueTest is assigned in the process DispatchQueue.main.async. Nevertheless, I get only a nil back when querying via ContentView.
You could set up your HealthStore class and use it as an EnvironmentObject. Assuming your app uses the SwiftUI lifecycle you can inject HealthStore into the environment in the #main entry point of your app.
import SwiftUI
#main
struct NameOfYourHeathApp: App {
let healthStore = HealthStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(healthStore)
}
}
}
Change your HealthStore class to this. (I removed your commented out code in my sample below)
import HealthKit
class HealthStore: ObservableObject {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
// I moved the HealthStore conditional check out of your View logic
// and placed it here instead.
func setUpHealthStore() {
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!
]
// I left the `toShare` as nil as I did not dig into adding bloodpressure reading to HealthKit.
healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
if success {
self.calculateBloodPressureSystolic()
}
})
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
Then use it in your ContentView like this.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var healthStore: HealthStore
var body: some View {
Text("Hello, world!")
.onAppear {
healthStore.setUpHealthStore()
}
}
}
I didn't go through the trouble of setting up the proper permissions in the .plist file, but you'll also need to set up the Health Share Usage Description as well as Health Update Usage Description. I assume you have already done this but I just wanted to mention it.
This is what I am trying to achieve:
class MyVC: UIViewController {
#State var myBoolState: Bool = false
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
myBoolState.sink { value in .... }.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(myBoolState: $myBoolState))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#Binding var myBoolState: Bool
var body: some View {
Button(action: {
myBoolState = true
}) {
Text("Push Me")
}
}
}
But the above of course does not compile.
So the question is: can I somehow declare a published property inside a view controller, pass it to a SwiftUI View and get notified when the SwiftUI view changes its value?
The #State wrapper works (by design) only inside SwiftUI view, so you cannot use it in view controller. Instead there is ObsevableObject/ObservedObject pattern for such purpose because it is based on reference types.
Here is a demo of possible solution for your scenario:
import Combine
class ViewModel: ObservableObject {
#Published var myBoolState: Bool = false
}
class MyVC: UIViewController {
let vm = ViewModel()
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
vm.$myBoolState.sink { value in
print(">> here it goes")
}.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(vm: self.vm))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#ObservedObject var vm: ViewModel
var body: some View {
Button(action: {
vm.myBoolState = true
}) {
Text("Push Me")
}
}
}
In my app, there is a singleton instance, AppSetting, which is used in the entire views and models. AppSetting has a variable, userName.
class AppSetting: ObservableObject {
static let shared = AppSetting()
private init() { }
#Published var userName: String = ""
}
ParentView prints userName when it is not empty. At first, it is empty.
struct ParentView: View {
#State var isChildViewPresented = false
#ObservedObject var appSetting = AppSetting.shared
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack {
Button(action: { self.isChildViewPresented = true }) {
Text("Show ChildView")
}
if !appSetting.userName.isEmpty { // <--- HERE!!!
Text("\(appSetting.userName)")
}
}
if isChildViewPresented {
ChildView(isPresented: $isChildViewPresented)
}
}
}
}
When a user taps the button, userName will be set.
struct ChildView: View {
#Binding var isPresented: Bool
#ObservedObject var childModel = ChildModel()
var body: some View {
ZStack {
Color.white.edgesIgnoringSafeArea(.all)
VStack {
Button(action: { self.childModel.setUserName() }) { // <--- TAP BUTTON HERE!!!
Text("setUserName")
}
Button(action: { self.isPresented = false }) {
Text("Close")
}
}
}
}
}
class ChildModel: ObservableObject {
init() { print("init") }
deinit { print("deinit") }
func setUserName() {
AppSetting.shared.userName = "StackOverflow" // <--- SET userName HERE!!!
}
}
The problem is when userName is set, the instance of ChildModel is invalidated. I think when ParentView adds Text("\(appSetting.userName)"), it changes its view hierarchy and then it makes SwiftUI delete the old instance of ChildModel and create a new one. Sadly, it gives me tons of bug. In my app, the ChildModel instance must be alive until a user explicitly closes ChildView.
How can I make the ChildModel instance alive?
Thanks in advance.
It is possible when to de-couple view & view model and inject dependency via constructor
struct ChildView: View {
#Binding var isPresented: Bool
#ObservedObject var childModel: ChildModel // don't initialize
// ... other your code here
store model somewhere externally and inject when show child view
if isChildViewPresented {
// inject ref to externally stored ChildModel()
ChildView(isPresented: $isChildViewPresented, viewModel: childModel)
}
I have an ObservableObject view model with a #Published variable that's an enum indicating the status of the viewModel:
class BatchesViewModel: ObservableObject {
#Published private var lookupStatus: BatchLookupStatus = .none
...
}
enum BatchLookupStatus {
case none
case lookingUp(searchText: String)
case error(errorMessage: String)
case validated(batch: Batch)
}
I set the status when there's an error, or there's a validation, etc. I set the status back to .none when the error is dismissed.
Everything works fine, except I get a EXC_BAD_ACCESS crash randomly.
I wanted to see what the status was when that happens, so I put a print statement when the status is set:
#Published private var lookupStatus: BatchLookupStatus = .none {
didSet {
print(_lookupStatus)
}
}
I noticed that with that print statement, the crash no longer happens. In fact, if I just leave an empty didSet, it prevents the crash as well:
#Published private var lookupStatus: BatchLookupStatus = .none { didSet {} }
As soon as I remove the didSet, it crashes again.
What in the world is going on?
I assume the reason is not in the provided model, because as tested it all works as expected - I mean with Published-Refresh flow.
Here is an example of your model usage I did:
import SwiftUI
import Combine
class BatchesViewModel: ObservableObject {
#Published var lookupStatus: BatchLookupStatus = .none
}
enum BatchLookupStatus {
case none
case lookingUp(searchText: String)
case error(errorMessage: String)
}
struct TestPublishCrashed: View {
#ObservedObject var viewModel = BatchesViewModel()
var body: some View {
VStack {
Button(action: {
self.viewModel.lookupStatus = .error(errorMessage: "Search failed")
}) {
Text("Emulate Error").padding().background(Color.yellow)
}.padding()
Button(action: {
self.viewModel.lookupStatus = .lookingUp(searchText: "hello world")
}) {
Text("Emulate Query").padding().background(Color.yellow)
}.padding()
status.padding()
}
}
var status: some View {
switch viewModel.lookupStatus {
case .none:
return Text("No status")
case .error(let error):
return Text("Got error: \(error)")
case .lookingUp(let query):
return Text("Searching: \(query)")
}
}
}
struct TestPublishCrashed_Previews: PreviewProvider {
static var previews: some View {
TestPublishCrashed()
}
}
For the following code, I am getting the following error. I don't know how to work around this. How can I call volumeCheck() upon the button click?
struct ContentView: View {
var player = AVAudioPlayer()
var body: some View {
HStack {
Button(action: {
self.volumeCheck()
}) {
Text("Click to test chimes volume")
}
}
}
mutating func volumeCheck() {
guard let url = Bundle.main.url(
forResource: "chimes",
withExtension: "mp3"
) else { return }
do {
player = try AVAudioPlayer(contentsOf: url)
player.prepareToPlay()
player.volume = Float(self.sliderValue)
player.play()
} catch let error {
print(error.localizedDescription)
}
print("volume checked print")
}
}
The problem is that View is a struct and it's body field is a computed property with a nonmutating getter. In your code it happens mutating method to be called in that nonmutating getter. So all you need to do is put your player to some kind of model:
class Model {
var player: AVPlayerPlayer()
}
struct ContentView: View {
var model = Model()
// player can be changed from anywhere
}
P.S. In some other cases you may want changes in your model be reflected in view so you'd have to add #ObservedObject just before model declaration.
Hope that helps
You are trying to set player to a new object in volumeCheck(). Set the player in your initialiser:
struct ContentView: View {
private var player: AVAudioPlayer?
public init() {
if let url = Bundle.main.url(forResource: "chimes",
withExtension: "mp3") {
self.player = try? AVAudioPlayer(contentsOf: url)
}
}
var body: some View {
HStack {
Button("Click to test chimes volume") {
self.volumeCheck()
} .disabled(player == nil)
}
}
private func volumeCheck() {
player?.prepareToPlay()
player?.volume = Float(self.sliderValue)
player?.play()
print("volume checked print")
}
}
Please note that you are using sliderValue in your code even though it is not defined anywhere.