How to associate a block of code with a #State var when it changes - swift

Whenever the value of the #State variable myData changes I would like to be notified and store that data in an #AppStorage variable myStoredData. However, I don't want to have to write explicitly this storing code everywhere the state var is changed, I would like to associate a block of code with it that gets notified whenever the state var changes and performs storage. The reason for this is, for example, I want to pass the state var as a binding to another view, and when that view would change the variable, the storage block would automatically be executed. How can I do this/can I do this in SwiftUI?
struct MyView : View {
#AppStorage("my-data") var myStoredData : Data!
#State var myData : [String] = ["hello","world"]
var body : some View {
Button(action: {
myData = ["something","else"]
myStoredData = try? JSONEncoder().encode(myData)
}) {
Text("Store data when button pressed")
}
.onAppear {
myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
}
}
}
I'm looking for something like this, but this does not work:
#State var myData : [String] = ["hello","world"] {
didSet {
myStoredData = try? JSONEncoder().encode(myData)
}
}

Simply use .onChange modifier anywhere in view (like you did with .onAppear)
struct MyView : View {
#AppStorage("my-data") var myStoredData : Data!
#State var myData : [String] = ["hello","world"]
var body : some View {
Button(action: {
myData = ["something","else"]
myStoredData = try? JSONEncoder().encode(myData)
}) {
Text("Store data when button pressed")
}
.onChange(of: myData) {
myStoredData = try? JSONEncoder().encode($0) // << here !!
}
.onAppear {
myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
}
}
}

You can add a set callback extension to the binding to monitor the value change
extension Binding {
/// When the `Binding`'s `wrappedValue` changes, the given closure is executed.
/// - Parameter closure: Chunk of code to execute whenever the value changes.
/// - Returns: New `Binding`.
func onChange(_ closure: #escaping () -> Void) -> Binding<Value> {
Binding(get: {
wrappedValue
}, set: { newValue in
wrappedValue = newValue
closure()
})
}
}
Use extension code
$myData.onChange({
})

Related

Passing a Binding variable to a SwiftUI view from a function causes didSet observer to no longer fire

I want to be able to dynamically return the right variable to be used as a #Binding in a SwiftUI view however when I create a function to return something as Binding I no longer receive the didSet call on that variable. I'm not sure if this is unsupported behavior or I'm doing something wrong.
Here is an example
struct ContentView: View {
#StateObject var dataStore = DataStore()
var body: some View {
VStack {
Toggle("val1", isOn: $dataStore.val1)
Toggle("val2", isOn: dataStore.boundVal2())
}
}
}
class DataStore: ObservableObject {
#AppStorage("val1")
var val1: Bool = false {
didSet {
print("did set val1")
}
}
#AppStorage("val2")
var val2: Bool = false {
didSet {
print("did set val2")
}
}
func boundVal2() -> Binding<Bool> {
return $val2
}
}
When you toggle the first value you get the didSet call, but when you toggle the second value you don't get it.
It turns out that you need to use a Binding object to pass it back, like so:
func boundVal2() -> Binding<Bool> {
Binding(
get: { self.val2 },
set: { self.val2 = $0 }
)
}

Bridging Optional Binding to Non-Optional Child (SwiftUI)

I have a parent state that might exist:
class Model: ObservableObject {
#Published var name: String? = nil
}
If that state exists, I want to show a child view. In this example, showing name.
If name is visible, I'd like it to be shown and editable. I'd like this to be two-way editable, that means if Model.name changes, I'd like it to push to the ChildUI, if the ChildUI edits this, I'd like it to reflect back to Model.name.
However, if Model.name becomes nil, I'd like ChildUI to hide.
When I do this, via unwrapping of the Model.name, then only the first value is captured by the Child who is now in control of that state. Subsequent changes will not push upstream because it is not a Binding.
Question
Can I have a non-optional upstream bind to an optional when it exists? (are these the right words?)
Complete Example
import SwiftUI
struct Child: View {
// within Child, I'd like the value to be NonOptional
#State var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
class Model: ObservableObject {
// within the parent, value is Optional
#Published var name: String? = nil
}
struct Parent: View {
#ObservedObject var model: Model = .init()
var body: some View {
VStack(spacing: 12) {
Text("Demo..")
// whatever Child loads the first time will retain
// even on change of model.name
if let text = model.name {
Child(text: text)
}
// proof that model.name changes are in fact updating other state
Text("\(model.name ?? "<waiting>")")
}
.onAppear {
model.name = "first change of optionality works"
loop()
}
}
#State var count = 0
func loop() {
async(after: 1) {
count += 1
model.name = "updated: \(count)"
loop()
}
}
}
func async(_ queue: DispatchQueue = .main,
after: TimeInterval,
run work: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + after, execute: work)
}
struct OptionalEditingPreview: PreviewProvider {
static var previews: some View {
Parent()
}
}
Child should take a Binding to the non-optional string, rather than using #State, because you want it to share state with its parent:
struct Child: View {
// within Child, I'd like the value to be NonOptional
#Binding var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
Binding has an initializer that converts a Binding<V?> to Binding<V>?, which you can use like this:
if let binding = Binding<String>($model.name) {
Child(text: binding)
}
If you're getting crashes from that, it's a bug in SwiftUI, but you can work around it like this:
if let text = model.name {
Child(text: Binding(
get: { model.name ?? text },
set: { model.name = $0 }
))
}
Bind your var like this. Using custom binding and make your child view var #Binding.
struct Child: View {
#Binding var text: String //<-== Here
// Other Code
if model.name != nil {
Child(text: Binding($model.name)!)
}

Using Combine to parse phone number String

I'm trying to wrap my mind around how Combine works. I believe I'm doing something wrong when I use the .assign operator to mutate the #Published property I'm operating on. I've read the documentation on Publishers, Subscribers, and Operators. But I'm a bit loose on where exactly to create the Publisher if I don't want it to be a function call.
import SwiftUI
import Combine
struct PhoneNumberField: View {
let title: String
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField(title,text: $viewModel.text)
}
class ViewModel: ObservableObject {
#Published var text: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$text.map { value -> String in
self.formattedNumber(number: value)
}
//something wrong here
.assign(to: \.text, on: self)
.store(in: &disposables)
}
func formattedNumber(number: String) -> String {
let cleanPhoneNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "+X (XXX) XXX-XXXX"
var result = ""
var index = cleanPhoneNumber.startIndex
for ch in mask where index < cleanPhoneNumber.endIndex {
if ch == "X" {
result.append(cleanPhoneNumber[index])
index = cleanPhoneNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
}
}
struct PhoneNumberParser_Previews: PreviewProvider {
static var previews: some View {
PhoneNumberField(title: "Phone Number")
}
}
Use .receive(on:):
$text.map { self.formattedNumber(number: $0) }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.text = value
})
.store(in: &disposables)
This will allow you to listen to changes of the text variable and update it in the main queue. Using main queue is necessary if you want to update #Published variables read by some View.
And to avoid having a retain cycle (self -> disposables -> assign -> self) use sink with a weak self.

String contents not being changed

This is my main view where I create an object of getDepthData() that holds a string variable that I want to update when the user click the button below. But it never gets changed after clicking the button
import SwiftUI
struct InDepthView: View {
#State var showList = false
#State var pickerSelectedItem = 1
#ObservedObject var data = getDepthData()
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.data.whichCountry = "usa"
print(" indepthview "+self.data.whichCountry)
}) {
Text("change value")
}
}
}
}
Here is my class where I hold a string variable to keep track of the country they are viewing. But when every I try to modify the whichCountry variable it doesn't get changed
class getDepthData: ObservableObject {
#Published var data : Specific!
#Published var countries : HistoricalSpecific!
#State var whichCountry: String = "italy"
init() {
updateData()
}
func updateData() {
let url = "https://corona.lmao.ninja/v2/countries/"
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url+"\(self.whichCountry)")!) { (data, _, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
let json = try! JSONDecoder().decode(Specific.self, from: data!)
DispatchQueue.main.async {
self.data = json
}
}.resume()
}
}
Any help would be greatly appreciated!
You need to define the whichCountry variable as #Published to apply changes on it
#Published var whichCountry: String = "italy"
You need to mark whichCountry as a #Published variable so SwiftUI publishes a event when this property have been changed. This causes the body property to reload
#Published var whichCountry: String = "italy"
By the way it is a convention to write the first letter of your class capitalized:
class GetDepthData: ObservableObject { }
As the others have mentioned, you need to define the whichCountry variable as #Published to apply changes to it. In addition you probably want to update your data because whichCountry has changed. So try this:
#Published var whichCountry: String = "italy" {
didSet {
self.updateData()
}
}

Convert a #State into a Publisher

I want to use a #State variable both for the UI and for computing a value.
For example, let's say I have a TextField bound to #State var userInputURL: String = "https://". How would I take that userInputURL and connect it to a publisher so I can map it into a URL.
Pseudo code:
$userInputURL.publisher()
.compactMap({ URL(string: $0) })
.flatMap({ URLSession(configuration: .ephemeral).dataTaskPublisher(for: $0).assertNoFailure() })
.eraseToAnyPublisher()
You can't convert #state to publisher, but you can use ObservableObject instead.
import SwiftUI
final class SearchStore: ObservableObject {
#Published var query: String = ""
func fetch() {
$query
.map { URL(string: $0) }
.flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
.sink { print($0) }
}
}
struct ContentView: View {
#StateObject var store = SearchStore()
var body: some View {
VStack {
TextField("type something...", text: $store.query)
Button("search") {
self.store.fetch()
}
}
}
}
You can also use onChange(of:) to respond to #State changes.
struct MyView: View {
#State var userInputURL: String = "https://"
var body: some View {
VStack {
TextField("search here", text: $userInputURL)
}
.onChange(of: userInputURL) { _ in
self.fetch()
}
}
func fetch() {
print("changed", userInputURL)
// ...
}
}
Output:
changed https://t
changed https://ts
changed https://tsr
changed https://tsrs
changed https://tsrst
The latest beta has changed how variables are published so I don't think that you even want to try. Making ObservableObject classes is pretty easy but you then want to add a publisher for your own use:
class ObservableString: Combine.ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<String, Never>()
var string: String {
willSet { objectWillChange.send() }
didSet { publisher.send(string) }
}
init(_ string: String = "") { self.string = string }
}
Instead of #State variables you use #ObservableObject and remember to access the property string directly rather than use the magic that #State uses.
After iOS 14.0, you can access to Publisher.
struct MyView: View {
#State var text: String?
var body: some View {
Text(text ?? "")
.onReceive($text.wrappedValue.publisher) { _ in
let publisher1: Optional<String>.Publisher = $text.wrappedValue.publisher
// ... or
let publisher2: Optional<String>.Publisher = _text.wrappedValue.publisher
}
}
}