I am saving an Int in UserDefaults and this is reduced by one by clicking a button. I don't know if that is important but I have added an extension to UserDefaults to load an initial value if the app starts for the first time:
extension UserDefaults {
public func optionalInt(forKey defaultName: String) -> Int? {
let defaults = self
if let value = defaults.value(forKey: defaultName) {
return value as? Int
}
return nil
}
}
The UserDefaults are used as ObservableObject and accessed as EnvironmentObject within the app like this:
class Preferences: ObservableObject {
#Published var counter: Int = UserDefaults.standard.optionalInt(forKey: COUNTER_KEY) ?? COUNTER_DEFAULT_VALUE {
didSet {
UserDefaults.standard.set(counter, forKey: COUNTER_KEY)
}
}
}
I am now trying to test that the value in the UserDefaults decreases when the button is clicked.
I am trying to read the UserDefaults in the test with:
XCTAssertEqual(UserDefaults.standard.integer(forKey: "COUNTER_KEY"), 9)// default is 10
I have tried it with normal UnitTests where the methods behind the Button are called and with UITests but both do not work. In the UnitTests I get back the COUNTER_DEFAULT_VALUE and in the UiTests I get back 0.
I am trying to access the UserDefaults directly in the test, instead of using the Preferences object, because I have not found a way to access that as it is an ObservableObject.
I have checked in the Emulator that the UserDefaults are saved/loaded correctly when using the app. Is it not possible to access the UserDefaults in the tests or am I doing it wrong?
The key to success is Dependency injection. Instead of directly accessing the shared user defaults object (UserDefaults.standard), declare an object of type UserDefaults within the class:
let userDefaults: UserDefaults
In the view where you declare the model, you are free to use the shared object:
#StateObject var model = Preferences(userDefaults: UserDefaults.standard)
But inside your test, create an dedicated UserDefaults object and pass it to the initializer like so:
let userDefaults = UserDefaults(suiteName: #file)!
userDefaults.removePersistentDomain(forName: #file)
let model = Preferences(userDefaults: userDefaults)
The benefit is clear: You control the state of UserDefaults. And that means the code works in every environment. To keep it simple, I haven't incorporated your extension, yet. But I'm sure you will manage to get it working.
TL;DR
Please see my working example below:
ContentView.swift
import SwiftUI
import Foundation
class Preferences: ObservableObject {
let userDefaults: UserDefaults
#Published var counter: Int {
didSet {
self.userDefaults.set(counter, forKey: "myKey")
}
}
init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
self.counter = userDefaults.integer(forKey: "myKey")
}
func decreaseCounter() {
self.counter -= 1
}
}
struct ContentView: View {
#StateObject var model = Preferences(userDefaults: UserDefaults.standard)
var body: some View {
HStack {
Text("Value: \(self.model.counter)")
Button("Decrease") {
self.model.decreaseCounter()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
InjectionTests.swift
import XCTest
#testable import Injection
class InjectionTests: XCTestCase {
func testPreferences() throws {
// arrange
let userDefaults = UserDefaults(suiteName: #file)!
userDefaults.removePersistentDomain(forName: #file)
let model = Preferences(userDefaults: userDefaults)
// act
let valueBefore = userDefaults.integer(forKey: "myKey")
model.decreaseCounter()
let valueAfter = userDefaults.integer(forKey: "myKey")
// assert
XCTAssertEqual(valueBefore - 1, valueAfter)
}
}
Related
I want to have a Bool property, that represents that option key is pressed #Publised var isOptionPressed = false. I would use it for changing SwiftUI View.
For that, I think, that I should use Combine to observe for key pressure.
I tried to find an NSNotification for that event, but it seems to me that there are no any NSNotification, that could be useful to me.
Since you are working through SwiftUI, I would recommend taking things just a step beyond watching a Publisher and put the state of the modifier flags in the SwiftUI Environment. It is my opinion that it will fit in nicely with SwiftUI's declarative syntax.
I had another implementation of this, but took the solution you found and adapted it.
import Cocoa
import SwiftUI
import Combine
struct KeyModifierFlags: EnvironmentKey {
static let defaultValue = NSEvent.ModifierFlags([])
}
extension EnvironmentValues {
var keyModifierFlags: NSEvent.ModifierFlags {
get { self[KeyModifierFlags.self] }
set { self[KeyModifierFlags.self] = newValue }
}
}
struct ModifierFlagEnvironment<Content>: View where Content:View {
#StateObject var flagState = ModifierFlags()
let content: Content;
init(#ViewBuilder content: () -> Content) {
self.content = content();
}
var body: some View {
content
.environment(\.keyModifierFlags, flagState.modifierFlags)
}
}
final class ModifierFlags: ObservableObject {
#Published var modifierFlags = NSEvent.ModifierFlags([])
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
self?.modifierFlags = event.modifierFlags
return event;
}
}
}
Note that my event closure is returning the event passed in. If you return nil you will prevent the event from going farther and someone else in the system may want to see it.
The struct KeyModifierFlags sets up a new item to be added to the view Environment. The extension to EnvironmentValues lets us store and
retrieve the current flags from the environment.
Finally there is the ModifierFlagEnvironment view. It has no content of its own - that is passed to the initializer in an #ViewBuilder function. What it does do is provide the StateObject that contains the state monitor, and it passes it's current value for the modifier flags into the Environment of the content.
To use the ModifierFlagEnvironment you wrap a top-level view in your hierarchy with it. In a simple Cocoa app built from the default Xcode template, I changed the application SwiftUI content to be:
struct KeyWatcherApp: App {
var body: some Scene {
WindowGroup {
ModifierFlagEnvironment {
ContentView()
}
}
}
}
So all of the views in the application could watch the flags.
Then to make use of it you could do:
struct ContentView: View {
#Environment(\.keyModifierFlags) var modifierFlags: NSEvent.ModifierFlags
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
if(modifierFlags.contains(.option)) {
Text("Option is pressed")
} else {
Text("Option is up")
}
}
.padding()
}
}
Here the content view watches the environment for the flags and the view makes decisions on what to show using the current modifiers.
Ok, I found easy solution for my problem:
class KeyPressedController: ObservableObject {
#Published var isOptionPressed = false
init() {
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event -> NSEvent? in
if event.modifierFlags.contains(.option) {
self?.isOptionPressed = true
} else {
self?.isOptionPressed = false
}
return nil
}
}
}
I have a view that gets a core data instance as an argument:
let timerObject: TimerObject
var body: some View {
Text(timerObject.name)
}
But now of course my preview throws an error because of course it needs a value for my view:
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
TimerView(timerObject: ???)
}
}
if it were simple values like a string that I have to pass in then of course I can use .constant(...). Only which test value can I pass here with a core data object? Can I perhaps pass a test instance from the persistence file or something like that?
I would be very happy to hear from you!
Best regards
Using the Xcode core data template, I did the following.
My model has create funcs for all objects. The principle is:
extension TimerObject {
static func create(
pos: Int = 0, // replace with your properties
title: String = "",
inContext ctx: NSManagedObjectContext
) -> Self {
let record = self.init(context: ctx)
record.id = UUID()
record.pos = Int16(pos) // replace with your properties
record.title = title
saveContext(context: ctx)
return record
}
}
Then you can use those in the previews:
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
// this is the Xcode generated preview container
let ctx = PersistenceController.preview.container.viewContext
let timer = TimerObject.create(pos: 0, title: "Test", inContext: ctx)
TimerView(timerObject: timer)
.environment(\.managedObjectContext, ctx)
}
}
I am trying to build a MVP with SwiftUI that simply shows me the changes in altitude on my Apple Watch. From there I will figure out where to go next (I want to use it for paragliding and other aviation things).
I have previous experience in python, but nothing in Swift, so even after a ton of tutorials I am very unsure about how and where to declare and then use functionalities.
Here is my code so far:
//
// ContentView.swift
// Altimeter WatchKit Extension
//
// Created by Luke Crouch on 29.09.20.
//
import SwiftUI
import CoreMotion
//class func isRelativeAltitudeAvailable() -> Bool
struct ContentView: View {
let motionManager = CMMotionManager()
let queue = OperationQueue()
let altimeter = CMAltimeter()
let altitude = 0
var relativeAltitude: NSNumber = 0
var body: some View {
if motionManager.isRelativeAltitudeAvailable() {
switch CMAltimeter.authorizationStatus() {
case .notDetermined: // Handle state before user prompt
fatalError("Awaiting user prompt...")
case .restricted: // Handle system-wide restriction
fatalError("Authorization restricted!")
case .denied: // Handle user denied state
fatalError("Auhtorization denied!")
case .authorized: // Ready to go!
print("Authorized!")
#unknown default:
fatalError("Unknown Authorization Status")
}
altimeter.startRelativeAltitudeUpdates(to: queue, withHandler: CMAltitudeHandler)
}
// something like relative Altitude = queue[..]
Text("\(relativeAltitude)")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.green)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am getting multiple errors that I dont know how to deal with:
Type 'Void' cannot conform to 'View', only struct/enum/class types can conform to protocols.
Value of type CMMotionManager has no member 'isRelativeAltitudeAvailable'
Type '()' cannot conform to View...
Cannot convert value of type 'CMAltitudeHandler.Type' (aka '((Optional, Optional) -> ()).Type') to expected argument type 'CMAltitudeHandler' (aka '(Optional, Optional) -> ()')
Could you please give me some hints?
Thank you so much!
Luke
I figured it out after trying around a lot:
//
// ContentView.swift
// Altimeter WatchKit Extension
//
// Created by Lukas Wheldon on 29.09.20.
//
import SwiftUI
import CoreMotion
struct ContentView: View {
#State var relativeAltitude: NSNumber = 0
#State var altitude = 0
let altimeter = CMAltimeter()
func update(d: CMAltitudeData?, e: Error?){
print("altitude \(altitude)")
print("CMAltimeter \(altimeter)")
print("relative Altitude \(relativeAltitude))")
}
var body: some View {
VStack {
Text("\(altimeter)")
.fontWeight(.bold)
.foregroundColor(Color.green)
Button(action: {
print("START")
self.startAltimeter()
}, label: {
Text("Start Altimeter")
.bold()
.foregroundColor(.green)
})
}
}
func startAltimeter() {
if CMAltimeter.isRelativeAltitudeAvailable() {
switch CMAltimeter.authorizationStatus() {
case .notDetermined: // Handle state before user prompt
print("bb")
//fatalError("Awaiting user prompt...")
case .restricted: // Handle system-wide restriction
fatalError("Authorization restricted!")
case .denied: // Handle user denied state
fatalError("Authorization denied!")
case .authorized: // Ready to go!
let _ = print("Authorized!")
#unknown default:
fatalError("Unknown Authorization Status")
}
self.altimeter.startRelativeAltitudeUpdates(to: OperationQueue.main) {(data,error) in DispatchQueue.main.async {
print("\(altitude)")
print("\(relativeAltitude)")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Next steps will be to check if I can access the barometer raw data and calculate altitudes on that.
Good evening,
I'm implementing a Realm Database with SwiftUI.
The Database is made of a table containing "Projects" and a table containing "Measures" (relation one-to-many).
The main view displays the project list and "Measureview" displays the measures related to the project selected.
When I select a project, the measures list is displayed and then impossible to go back, the app crashes (simulator and real device).
Xcode points AppDelegate file : Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffedfd5cfd8)
I know that 4/5 months ago, some developers experienced this issue but I suppose that currently Apple fix the problem.
Please find below the relevant code :
Realm part :
import Foundation
import RealmSwift
import Combine
class Project : Object, Identifiable {
#objc dynamic var ProjectName = "" // primary key
#objc dynamic var ProjectCategorie = ""
#objc dynamic var ProjectCommentaire = ""
let Measures = List<Measure>() // one to many
override static func primaryKey() -> String? {
return "ProjectName"
}
}
class Measure : Object, Identifiable {
// #objc dynamic var id_Measure = UUID().uuidString // primary key
#objc dynamic var MeasureName = ""
#objc dynamic var MeasureDetail = ""
#objc dynamic var MeasureResult = ""
override static func primaryKey() -> String? {
return "MeasureName"
}
}
func createProject (_ title:String,_ categorie:String, _ commentaire:String) {
let realm = try! Realm()
let proj = Project()
proj.ProjectName = title
proj.ProjectCategorie = categorie
proj.ProjectCommentaire = commentaire
try! realm.write {
realm.add(proj)
}
}
//****************************************************************
class BindableResults<Element>: ObservableObject where Element: RealmSwift.RealmCollectionValue {
let didChange = PassthroughSubject<Void, Never>()
let results: Results<Element>
private var token: NotificationToken!
init(results: Results<Element>) {
self.results = results
lateInit()
}
func lateInit() {
token = results.observe { _ in
self.didChange.send(())
}
}
deinit {
token.invalidate()
}
}
Contentview :
struct ContentView : View {
#ObservedObject var Proj = BindableResults(results: try! Realm().objects(Project.self))
var body: some View {
NavigationView{
List(Proj.results) { item in
NavigationLink(destination: MeasureView(Pro: item) ){
ContenRowUI(Proj :item)
}
}
}
.navigationBarTitle(Text("Project List"))
}
}
Measure view :
struct MeasureView: View {
var Pro = Project() //= Project()
var body: some View {
NavigationView {
List(Pro.Measures) { item in
Text("Detail: \(item.MeasureDetail)")
}
.navigationBarTitle(Text("Measure"))
}
}
}
Additional information, if I replace Measureview by a simple textview, the behaviour is very weird :
I select a Project, the application shows the textview and goes back automatically to the main list (project list)
If someone could help me, I would be grateful.
Thanks a lot for your support.
Jeff
I am trying to understand #EnvironmentObject better so I wrote sample code below to replicate the issue i am facing
This is the class where i declare the array which needs to be accessed in multiple locations and be displayed and updated in ContentView
class User: ObservableObject {
#Published var array = [String]()
func diplayName(name: String){
self.array.append(name)
}
}
I want to be able to append my array in another class. Something like the below code
class myTests: ObservableObject {
#EnvironmentObject var user:User
func diplayMyName(name: String){
self.user.array.append(name)
}
}
When I call displayMyName function in myTests class i get an Error message as below
Fatal error: No ObservableObject of type User found.
A View.environmentObject(_:) for User may be missing as an ancestor of this view.
This is how my contentView looks like
struct ContentView: View {
#EnvironmentObject var user:User
var testing = myTests()
var body: some View {
VStack {
List(user.array, id: \.self){ x in
Text(x)
}
Button(action: {
self.user.diplayName(name: "Name1")
// self.testing.diplayMyName(name: "Name2")
}){
Text("Call Function")
}
}
}
}
This is how i declare my environment object in scene delegate
let contentView = ContentView().environmentObject(User())
I would really appreciate if someone can help me understand why am i getting the error when i append the published array from myTests class. Thank you.
UPDATE
To work around my issue i did the following adjustments
I returned an array in myTests class
class myTests {
var ar = [String]()
func displayMyName() -> [String] {
ar.removeAll()
ar.append(contentsOf: ["Name2", "Name3"])
return ar
}
}
And added it to the array in ContentView
struct ContentView: View {
#EnvironmentObject var user : User
var testing = myTests()
var body: some View {
VStack {
List(user.array, id: \.self){ x in
Text(x)
}
Button(action: {
self.user.array.append(contentsOf: self.testing.displayMyName())
}){
Text("Call Function")
}
}
}
}