I came up with an implementation that mostly meets my needs. I've described it here: https://developer.apple.com/forums/thread/756192
Post
Replies
Boosts
Views
Activity
I found an implementation I'm mostly happy with.
First, I defined a Notification.Name and created a userDefaults binding 'factory'
extension Notification.Name {
static let userDefaultsChanged = Notification.Name(rawValue: "user.defaults.changed")
}
struct BindingFactory {
static func binding(for defaultsKey: String) -> Binding<Bool> {
return Binding {
return UserDefaults.standard.bool(forKey: defaultsKey)
} set: { newValue in
UserDefaults.standard.setValue(newValue, forKey: defaultsKey)
NotificationCenter.default.post(name: .userDefaultsChanged, object: defaultsKey)
}
}
}
Then in my UI elements that were previously using @AppStorage, they now use the new bindings.
var body: some View {
VStack {
Toggle("Extended", isOn: BindingFactory.binding(for: "extended"))
Text(LanguageManager.shared.summary)
}
}
And now LanguageManager's init can add a subscriber to the userDefaultsChanged notification.
init() {
NotificationCenter.default.publisher(for: .userDefaultsChanged)
.sink(receiveValue: { notification in
print("\(notification.object!) changed")
self.updateItems()
})
.store(in: &subscriptions)
}
The main thing I don't really like about this is the need to create a static func binding(for defaultsKey: String) -> Binding<Bool> for each type of binding (Bool, String, Int, etc.)
Hi again @Claude31
turns out I was curious enough about this to make time to investigate it sooner.
In my sample code, neither the View's willSet/didSet nor the class's willSet/didSet are getting called.
import SwiftUI
struct ContentView: View {
@AppStorage("extended") var extended: Bool = true {
willSet {
print("ConteentView willSet")
}
didSet {
print("ContentView didSet")
}
}
var body: some View {
VStack {
Toggle("Extended", isOn: $extended)
Text(LanguageManager.shared.summary)
}
.padding()
}
}
#Preview {
ContentView()
}
class LanguageManager {
static let shared = LanguageManager()
var items: [String] = ["basic", "list"]
var summary: String {
return items.reduce("") {
return "\($0)\n\($1)"
}
}
@AppStorage("extended") var extended: Bool = true {
willSet {
print("LanguageManager willSet")
}
didSet {
print("LanguageManager didSet")
updateItems()
}
}
func updateItems() {
if extended {
items = ["much", "more", "than", "basic", "list"]
} else {
items = ["basic", "list"]
}
}
}
**
Update**: I now see I should have followed the link you included. Sorry about not doing that before answering.
However this answer suggests that when a user updates the UI, the @AppStorage var in my class will most definitely not be getting updated. And that is the one that I'm hoping will be able to be notified when 'somebody else' updates one of the UserDefault values.
I want to define multiple UserDefault values that can be updated via settings/prefs UI.
and then when any of these values change, LanguageManager will be notified (pub/sub?) of the change, and run updateItems()
Good suggestion @Claude31. I just tried adding this:
struct UserDefaultsKey: EnvironmentKey {
static var defaultValue: UserDefaults = .standard
}
extension EnvironmentValues {
var userDefaults: UserDefaults {
get { self[UserDefaultsKey.self] }
set { self[UserDefaultsKey.self] = newValue }
}
}
and updating the original to include this:
@Environment(\.userDefaults) var userDefaults
@AppStorage("enhanced", store: userDefaults) var scriptPickers: Bool = true
I now get the following error one the @AppStorage line:
Cannot use instance member 'userDefaults' within property initializer; property initializers run before 'self' is available
bummer...
I think I've found something that works.
Moved CombineOptions into the model, and subscribe to the options changes in the Model's init.
Still, I'd appreciate any feedback on this code below.
struct ContentView: View {
@ObservedObject var model = Model()
var body: some View {
VStack {
Image(uiImage: model.composed)
.resizable()
.aspectRatio(contentMode: .fit)
Slider(value: $model.options.scale)
Stepper(value: $model.options.numberOfImages, label:
{
Text("\(model.options.numberOfImages)")})
}
.padding()
}
private var enhancedImage: UIImage {
return model.inputImage.combine(options: model.options)
}
}
class Model: ObservableObject {
let inputImage: UIImage = UIImage.init(named: "IMG_4097")!
@Published var options = CombineOptions.basic
@Published var composed: UIImage
private var cancellables: [AnyCancellable] = []
init() {
self.composed = inputImage
$options
.debounce(for: 1.0, scheduler: DispatchQueue.global(qos: .background))
.map( { mapOptions in
return UIImage.composed(from: self.inputImage, using: mapOptions)
} )
.receive(on: DispatchQueue.main)
.assign(to: \.composed, on: self)
.store(in: &cancellables)
}
}
Heavy Sigh on this missing functionality.
When I tried moving my dev schema to production, it failed and didn't give a reason.
Attempting to debug, I created a second container just by adding a 2 at the end of the identifier.
the new container works fine, so I tried deleting all the Record Types from the original. I then created a new recordType with a single custom field. it still fails with no reason given.
Then I thought I'd just delete and recreate the original container......
apparently not :-(
When a property of an element (value type) of an Array is modified, Swift treats as the Array itself is modified. That's the nature of value type in Swift. This nicely summarizes my main take away message from this exchange.
thx again!
Yes. Have you heard that Swift Array is a value type? I was not aware. For quite some time I've imagined Swift arrays more like NSArray
> initially: filter.tangleTypes = [0x123456, 0x123466]
What do you mean? What are 0x123456...?
Are you talking about the case of Array containing struct (value type)? This was my attempt to: better understand how value types are stored in array
the mechanism used for swiftUI to recognize that an @Published property has changed
0x123456 was intended to be a memory location (address)
I was under the impression that a swift array containing structs would be represented as a list of memory locations (one per struct/item in the array)
Further, I was imagining that when the item/struct was modified the array (modelled as a list of memLocations) would now include a new location for the modified item. (and that this new address in the array was what would allow swiftUI View classes to be made aware of the changed in the @Published property in the appropriate ObservableObject.
I recently learned that value types in an array don't really have a memory location.
https://forums.swift.org/t/memory-address-of-value-types-and-reference-types/6637/7
I guess they're just stored directly in the array's allocated memory? As such when the value object changes in anyway, the array's memory/contents are modified, and detected by any swiftUI View classes that happen to be listening.
Mike
Hi OOPer,
This was definitely not obvious (I'm gonna reserve judgement on whether this says more about me or the complexity of my question)
having said that, I think I now see:
when SwiftUI depends on properties in 'child' model objects stored in an array in a parent model object: the parent should conform to ObservableObject (and be a ref type)
the children in the array need to be value type (struct)
does this mean that updating a property in a value type that is stored in an array will replace the array element with a new copy of the value type.
initially: filter.tangleTypes = [0x123456, 0x123466]
filter.tangleTypes[1].isChecked = true
Are you saying at this point filter.tangleTypes = [0x123456, 0x123476]
?
as always thanks for taking the time to share :-)
Thanks OOPer, that is exactly the answer I was looking for.
(and fair point about the 'lazy' configuration of the DateFormatter :-)
Thanks OOPer. (and thanks for keeping my Canadian spelling of favourite in your sample code :-)
hmm, some progress. things compile now that i've changed phraseChangePublisher to this:
var phraseChangedPublisher: AnyCancellable {
Publishers.CombineLatest(leftWord.$currentWord, rightWord.$currentWord)
.map({
return $0.0 + $0.1
})
.debounce(for: 1.0, scheduler: RunLoop.main)
.sink { someValue in
print("someValue: \(someValue)")
}
}
Is AnyCancellable the best way to proceed here?
At this point the pipeline wasn't firing, even when the words were changing.
When I added the following ivar in my View:
let pipeline: AnyCancellable
init(model: PhraseModel) {
self.model = model
self.pipeline = model.phraseChangedPublisher
}
the pipeline code runs and behaves as expected. However when I change body to the following:
var body: some View {
HStack {
Text(model.leftWord.currentWord)
Text(model.rightWord.currentWord)
}
.onReceive(pipeline, perform: {_ in
print("hello")
})
}
I get a compile error at line2: "Unable to infer complex closure return type; add explicit type to disambiguate"
Any guidance on how to improve this?
(heavy sigh)
Nice I like that. Thanks.
While I'm waiting for swiftUI 2 to get out of beta, I decided to use this:
@State private var selectedTabIndex = DefaultsManager.shared.selectedTabIndex
var body: some View {
TabView(selection: $selectedTabIndex){
SearchTabView()
.tabItem{
Text("Search")
}
.tag(0)
.onAppear(){
DefaultsManager.shared.selectedTabIndex = 0
}
SecondTabView()
.tabItem{
Text("Second")
}
.tag(1)
.onAppear(){
DefaultsManager.shared.selectedTabIndex = 1
}
And tolerate the tech debt of ensuring that tag values match the index values.
This is a duplicate of https://developer.apple.com/forums/thread/658465
thanks BabyJ. That did the trick!