ObservableObject with NWPathMonitor - swiftui

I try to make NWPathMonitor an observable object that returns true if there is a network connection and false if not.
Can you help me because my solution doesn't work.
Thanks
import Foundation
import Network
class TestNetStatus: ObservableObject {
let monitor = NWPathMonitor()
let queue = DispatchQueue.global(qos: .background)
#Published var connected: Bool = false
private var isConnected: Bool = false
init() {
monitor.start(queue: queue)
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
self.isConnected = true
} else {
self.isConnected = false
}
}
self.connected = isConnected
}
}

you have to set the published variable on the main thread
class TestNetStatus: ObservableObject {
let monitor = NWPathMonitor()
let queue = DispatchQueue.global(qos: .background)
#Published var connected: Bool = false
private var isConnected: Bool = false
init() {
monitor.start(queue: queue)
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
OperationQueue.main.addOperation {
self.isConnected = true
self.connected = self.isConnected
}
} else {
OperationQueue.main.addOperation {
self.isConnected = false
self.connected = self.isConnected
} }
}
}
}
struct ContentView: View {
#EnvironmentObject var data : TestNetStatus
var body: some View {
VStack {
Text ("Status")
Text(data.connected ? "Connected" : "not connected")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(TestNetStatus())
}
}

Related

data from ObservableObject class do not pass to .alert()

Sorry for simple question, try to learn SwiftUI
My goal is to show alert then i can not load data from internet using .alert()
the problem is that my struct for error actually has data but it does not transfer to .alert()
debug shows that AppError struct fill in with error but then i try to check for nil or not it is always nil in .Appear()
PostData.swift
struct AppError: Identifiable {
let id = UUID().uuidString
let errorString: String
}
NetworkManager.swift
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
#Published var appError: AppError? = nil
func fetchGuardData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decorder = JSONDecoder()
if let safeData = data {
do {
let results = try decorder.decode(Results.self, from: safeData)
DispatchQueue.main.sync {
self.posts = results.hits }
} catch {
self.appError = AppError(errorString: error.localizedDescription)
}
} else {
self.appError = AppError(errorString: error!.localizedDescription)
}
} else {
DispatchQueue.main.sync {
self.appError = AppError(errorString: error!.localizedDescription)
}
}
} //
task.resume()
} else {
self.appError = AppError(errorString: "No url response")
}
}
}
ContentView.swift
struct ContentView: View {
#StateObject var networkManager = NetworkManager()
#State var showAlert = false
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4NEWS")
}
.onAppear() {
networkManager.fetchGuardData()
if networkManager.appError != nil {
showAlert = true
}
}
.alert(networkManager.appError?.errorString ?? "no data found", isPresented: $showAlert, actions: {})
}
}
Probably when doing this check, the data fetch process is not finished yet.
if networkManager.appError != nil {
showAlert = true
}
So you should wait the network request finish to check if there is error or not.
If you sure there is error and just test this try this to see error:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if networkManager.appError != nil {
showAlert = true
}
}
To handle better this situation you can pass a closure your fetchGuardData function and handle your result and error inside it.
or you can use .onChange for the listen the changes of appError.
.onChange(of: networkManager.appError) { newValue in }

How to make a drag and drop viewmodifier accept any object

I have created a drag-and-drop viewmodifier that works as expected, but now I would like to make it accept any object. I can add <T: Identifiable> to all the functions, structs, and view-modifiers, but when I try to do add it to my singleton class, I get "Static stored properties not supported in generic types".
I need the singleton class, so I can put the .dropObjectOutside viewmodifier anywhere in my view-hierarchy, so I've tried downcasting the ID to a String, but I can't seem to make that work.
Is there a way to downcast or make this code accept any object?
import SwiftUI
// I want this to be any object
struct StopContent: Identifiable {
var id: String = UUID().uuidString
}
// Singleton class to hold drag state
class DragToReorderController: ObservableObject {
// Make it a singleton, so it can be accessed from any view
static let shared = DragToReorderController()
private init() { }
#Published var draggedID: String? // How do I make this a T.ID or downcast T.ID to string everywhere else?
#Published var dragActive:Bool = false
}
// Add ViewModifier to view
extension View {
func dragToReorder(_ item: StopContent, array: Binding<[StopContent]>) -> some View {
self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
}
func dropOutside() -> some View {
self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
}
}
import UniformTypeIdentifiers
// MARK: View Modifier
struct DragToReorderObject: ViewModifier {
let sourceItem: StopContent
#Binding var contentArray: [StopContent]
#ObservedObject private var dragReorder = DragToReorderController.shared
func body(content: Content) -> some View {
content
.onDrag {
dragReorder.draggedID = sourceItem.id
dragReorder.dragActive = false
return NSItemProvider(object: String(sourceItem.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
.onChange(of: dragReorder.dragActive, perform: { value in
if value == false {
// Drag completed
}
})
.opacity(dragReorder.draggedID == sourceItem.id && dragReorder.dragActive ? 0 : 1)
}
}
// MARK: Drop and reorder
struct DropObjectDelegate: DropDelegate {
let sourceItem: StopContent
#Binding var listData: [StopContent]
#Binding var draggedItem: String?
#Binding var dragActive: Bool
func dropEntered(info: DropInfo) {
if draggedItem == nil { draggedItem = sourceItem.id }
dragActive = true
// Make sure the dragged item has moved and that it still exists
if sourceItem.id != draggedItem {
if let draggedItemValid = draggedItem {
if let from = listData.firstIndex(where: { $0.id == draggedItemValid } ) {
// If that is true, move it to the new location
let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
if listData[to].id != draggedItem! {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
dragActive = false
draggedItem = nil
return true
}
}
// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {
// Using a singleton so we can drop anywhere
#ObservedObject private var dragReorder = DragToReorderController.shared
func dropEntered(info: DropInfo) {
dragReorder.dragActive = true
}
func performDrop(info: DropInfo) -> Bool {
dragReorder.dragActive = false
dragReorder.draggedID = nil
return true
}
}
For this, you have to add Identifiable generic constraint everywhere. Also, use Int for draggedID instead of String.
Here is the demo code.
// Singleton class to hold drag state
class DragToReorderController: ObservableObject {
// Make it a singleton, so it can be accessed from any view
static let shared = DragToReorderController()
private init() { }
#Published var draggedID: Int?
#Published var dragActive: Bool = false
}
// Add ViewModifier to view
extension View {
func dragToReorder<T: Identifiable>(_ item: T, array: Binding<[T]>) -> some View {
self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
}
func dropOutside() -> some View {
self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
}
}
import UniformTypeIdentifiers
// MARK: View Modifier
struct DragToReorderObject<T: Identifiable>: ViewModifier {
let sourceItem: T
#Binding var contentArray: [T]
#ObservedObject private var dragReorder = DragToReorderController.shared
func body(content: Content) -> some View {
content
.onDrag {
dragReorder.draggedID = sourceItem.id.hashValue
dragReorder.dragActive = false
return NSItemProvider(object: String(sourceItem.id.hashValue) as NSString)
}
.onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
.onChange(of: dragReorder.dragActive, perform: { value in
if value == false {
// Drag completed
}
})
.opacity((dragReorder.draggedID == sourceItem.id.hashValue) && dragReorder.dragActive ? 0 : 1)
}
}
// MARK: Drop and reorder
struct DropObjectDelegate<T: Identifiable>: DropDelegate {
let sourceItem: T
#Binding var listData: [T]
#Binding var draggedItem: Int?
#Binding var dragActive: Bool
func dropEntered(info: DropInfo) {
if draggedItem == nil { draggedItem = sourceItem.id.hashValue }
dragActive = true
// Make sure the dragged item has moved and that it still exists
if sourceItem.id.hashValue != draggedItem {
if let draggedItemValid = draggedItem {
if let from = listData.firstIndex(where: { $0.id.hashValue == draggedItemValid } ) {
// If that is true, move it to the new location
let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
if listData[to].id.hashValue != draggedItem! {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
dragActive = false
draggedItem = nil
return true
}
}
// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {
// Using a singleton so we can drop anywhere
#ObservedObject private var dragReorder = DragToReorderController.shared
func dropEntered(info: DropInfo) {
dragReorder.dragActive = true
}
func performDrop(info: DropInfo) -> Bool {
dragReorder.dragActive = false
dragReorder.draggedID = nil
return true
}
}

SwiftUI: How to publish a variable in a class member object (another instance of a class) and update UI in View

I have a class PlayAudio to read an audio file and play. In PlayAudio, I have #objc updateUI function to add to CADisplayLink. I have another class Updater where I initialize and control isPaused of CADisplayLink. I've instantiated #Published var playAudio: PlayAudio so I can call it from View as updater.playAudio. My question is, although I can print playAudio.positionSliderValue real time in active CADisplayLink, playAudio.positionSliderValue does not update the UI in View. How can I achieve it? I want to activate and deActivate CADisplayLink from a separate class to maintain weak ownership (If I'm not mistaken...).
When #State var volume is updated, volume slider also updates, so I think I'm successfully updating the value itself, but I can't figure it out that update to trigger updates in UI. Any thoughts or suggestions are appreciated. Thanks.
import SwiftUI
import AVKit
struct ContentView: View {
#ObservedObject var updater = Updater()
#State var volume = 0.0
var body: some View {
Text("\(volume)")
VStack {
Slider(value:
// in order to get continuous value changes, I do this instead of $updater.playAudio.volumeSliderValue
Binding(get: {
updater.playAudio.volumeSliderValue
}, set: { (newValue) in
updater.playAudio.volumeSliderValue = newValue
updater.playAudio.setVolume()
volume = newValue
})
, in: 0...1)
Button(action: {
updater.playAudio.play()
// activate CADisplayLink
updater.activate()
// run CADisplayLink
updater.updater?.isPaused = false
}, label: {
Text("Play File")
})
Slider(value:
// in order to get continuous value changes, I do this instead of $playAudio.positionSliderValue
Binding(get: {
updater.playAudio.positionSliderValue
}, set: { (newValue) in
updater.playAudio.positionSliderValue = newValue
updater.playAudio.seek()
})
, in: 0.0...updater.playAudio.positionSliderTotal) { _ in
updater.playAudio.seek()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Updater: ObservableObject {
var updater: CADisplayLink?
#Published var playAudio: PlayAudio
init(){
self.playAudio = PlayAudio()
self.updater = CADisplayLink(target: playAudio, selector: #selector(playAudio.updateUI))
}
func activate() {
self.updater?.add(to: .main, forMode: .default)
}
func deActivate() {
self.updater?.invalidate()
}
}
class PlayAudio: ObservableObject {
var sampleRate = Double()
var totalFrame = AVAudioFramePosition()
var startTime = AVAudioTime()
var newFramePosition = AVAudioFramePosition()
let url = Bundle.main.urls(forResourcesWithExtension: "mp4", subdirectory: nil)?.first
var audioFile = AVAudioFile()
var engine = AVAudioEngine()
var avAudioPlayerNode = AVAudioPlayerNode()
#Published var volumeSliderValue: Double = 0.7
#Published var positionSliderTotal: Double = 0.0
#Published var positionSliderValue: Double = 0.0
#objc func updateUI() {
positionSliderValue = Double(currentFrame)
// this prints ok, but I want it to update the UI in the View
print(positionSliderValue)
}
init () {
readFile()
schedulePlayer()
getTotalFrameDouble()
}
var currentFrame: AVAudioFramePosition {
guard let lastRenderTime = avAudioPlayerNode.lastRenderTime,
let playerTime = avAudioPlayerNode.playerTime(forNodeTime: lastRenderTime)
else {
return 0
}
return playerTime.sampleTime + newFramePosition
}
func getTotalFrameDouble() {
positionSliderTotal = Double(totalFrame)
print(positionSliderValue)
}
func readFile() {
guard let url = url else {
return
}
do {
self.audioFile = try AVAudioFile(forReading: url)
} catch let error {
print(error)
}
self.sampleRate = audioFile.processingFormat.sampleRate
self.totalFrame = audioFile.length
}
func setupEngine() {
engine.attach(avAudioPlayerNode)
engine.connect(avAudioPlayerNode, to: engine.mainMixerNode, format: audioFile.processingFormat)
engine.prepare()
do {
try engine.start()
} catch let error {
print(error)
}
}
func schedulePlayer() {
newFramePosition = 0
engine.reset()
setupEngine()
avAudioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
}
func play() {
let outputFormat = avAudioPlayerNode.outputFormat(forBus: AVAudioNodeBus(0))
let lastRenderTime = avAudioPlayerNode.lastRenderTime?.sampleTime ?? 0
// need to convert from AVAudioFramePosition to AVAudioTime
startTime = AVAudioTime(sampleTime: AVAudioFramePosition(Double(lastRenderTime)), atRate: Double(outputFormat.sampleRate))
avAudioPlayerNode.play(at: startTime)
}
func seek() {
// player time (needs to be converted to player node time
newFramePosition = AVAudioFramePosition(positionSliderValue)
let framesToPlay = totalFrame - newFramePosition
avAudioPlayerNode.stop()
if framesToPlay > 100 {
avAudioPlayerNode.scheduleSegment(audioFile, startingFrame: newFramePosition, frameCount: AVAudioFrameCount(framesToPlay), at: nil, completionHandler: nil)
}
play()
}
func setVolume() {
avAudioPlayerNode.volume = Float(volumeSliderValue)
}
}

SwiftUI Picker desn't bind with ObservedObject

I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}

How I can save picker data with UserDefaults in SwiftUI?

I'm trying to save the user choice from picker component in UserDefault in SwiftUI but in contrary of a simple toggle, I'am blocked.
My View:
import SwiftUI
enum VibrationType: String, CaseIterable {
case low = "Faible"
case normal = "Normal"
case hight = "Fort"
}
struct CompassSettings: View {
#ObservedObject var settingsStore: SettingsStore = SettingsStore()
#State var vibrationIntensityIndex = 1
var body: some View {
VStack {
Toggle(isOn: $settingsStore.vibrationActivate) {
Text("Activer la vibration")
.font(.system(size: 18))
.foregroundColor(Color("TextDark"))
}
.padding(.top, 6)
Picker("Intesité de la vibration", selection: self.$settingsStore.vibrationIntensity) {
ForEach(0..<self.vibrationIntensity.count) { intensity in
Text(self.vibrationIntensity[intensity]).tag(intensity)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
SettingsStore class:
import SwiftUI
import Combine
final class SettingsStore: ObservableObject {
let vibrationIsActivate = PassthroughSubject<Void, Never>()
let intensityOfVibration = PassthroughSubject<Void, Never>()
// Vibration
var vibrationActivate: Bool = UserDefaults.vibrationActivated {
willSet {
UserDefaults.vibrationActivated = newValue
vibrationIsActivate.send()
}
}
// Vibration intensity
var vibrationIntensity: VibrationType = .normal {
willSet {
UserDefaults.vibrationIntensity = newValue
print(UserDefaults.vibrationIntensity)
intensityOfVibration.send()
}
}
}
The UserDefault extension:
extension UserDefaults {
private struct Keys {
static let vibrationActivated = "vibrationActivated"
static let vibrationIntensity = "vibrationIntensity"
}
// Vibration
static var vibrationActivated: Bool {
get { return UserDefaults.standard.bool(forKey: Keys.vibrationActivated) }
set { UserDefaults.standard.set(newValue, forKey: Keys.vibrationActivated) }
}
// Vibration intensity
static var vibrationIntensity: VibrationType {
get { return UserDefaults.standard.object(forKey: Keys.vibrationIntensity) as!
VibrationType ?? "normal" }
set { UserDefaults.standard.set(newValue, forKey: Keys.vibrationIntensity) }
}
}
So I have some errors in my UserDefaults extension. I don't know how I can save multiple string choices and how I can display a default choice.
Find below fixed code... tested with Xcode 11.2 / iOS 13.2.
enum VibrationType: String, CaseIterable {
case low = "Faible"
case normal = "Normal"
case hight = "Fort"
}
struct CompassSettings: View {
#ObservedObject var settingsStore: SettingsStore = SettingsStore()
#State var vibrationIntensityIndex = 1
#State var vibrationIntensity: [VibrationType] = [.low, .normal, .hight]
var body: some View {
VStack {
Toggle(isOn: $settingsStore.vibrationActivate) {
Text("Activer la vibration")
.font(.system(size: 18))
.foregroundColor(Color("TextDark"))
}
.padding(.top, 6)
Picker("Intesité de la vibration", selection: self.$settingsStore.vibrationIntensity) {
ForEach(self.vibrationIntensity, id: \.self) { intensity in
Text(intensity.rawValue).tag(intensity)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
final class SettingsStore: ObservableObject {
let vibrationIsActivate = PassthroughSubject<Void, Never>()
let intensityOfVibration = PassthroughSubject<Void, Never>()
// Vibration
var vibrationActivate: Bool = UserDefaults.vibrationActivated {
willSet {
UserDefaults.vibrationActivated = newValue
vibrationIsActivate.send()
}
}
// Vibration intensity
var vibrationIntensity: VibrationType = UserDefaults.vibrationIntensity {
willSet {
UserDefaults.vibrationIntensity = newValue
print(UserDefaults.vibrationIntensity)
intensityOfVibration.send()
}
}
}
extension UserDefaults {
private struct Keys {
static let vibrationActivated = "vibrationActivated"
static let vibrationIntensity = "vibrationIntensity"
}
// Vibration
static var vibrationActivated: Bool {
get { return UserDefaults.standard.bool(forKey: Keys.vibrationActivated) }
set { UserDefaults.standard.set(newValue, forKey: Keys.vibrationActivated) }
}
// Vibration intensity
static var vibrationIntensity: VibrationType {
get {
if let value = UserDefaults.standard.object(forKey: Keys.vibrationIntensity) as? String {
return VibrationType(rawValue: value)!
}
else {
return .normal
}
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Keys.vibrationIntensity)
}
}
}
From iOS 14, using onChange()
#State private var index = UserDefaults.standard.integer(forKey: "YourKey")
Picker(...)
.onChange(of: index) { newValue in
// Run code to save
UserDefaults.standard.set(newValue, forKey: "YourKey")
}
You have to save the raw value of the enum, the string.
The getter is a bit extensive but safe
// Vibration intensity
static var vibrationIntensity: VibrationType {
get {
if let vibrationRawValue = UserDefaults.standard.string(forKey: Keys.vibrationIntensity),
let type = VibrationType(rawValue: vibrationRawValue) {
return type
} else {
return VibrationType(rawValue: "Normal")!
}
}
set { UserDefaults.standard.set(newValue.rawValue, forKey: Keys.vibrationIntensity) }
}