Swift - didSet property not called from binding changes - ios

My problem:
I want to call a function when my customView clicked
So I use #State variable to observe the click action from my custom view. But the problem is my value changed but didSet function not triggered
My code:
Main struct:
#State var buttonClicked : Bool = false {
didSet{
needToCallMyFunction() // not triggered
}
}
Custom struct:(for my customview)
#Binding var isClicked : Bool
someview
.onTapGesture(perform: {
print("custom clicked")
isClicked.toggle()
})

Use instead in main view .onChange(of:) modifier, like
SomeView()
.onChange(of: buttonClicked) { _ in
self.needToCallMyFunction()
}
Update: variant for SwiftUI 1.0 / iOS 13+
import Combine // needed to use Just
...
SomeView()
.onReceive(Just(buttonClicked)) { _ in
self.needToCallMyFunction()
}

Asperi solution working fine.
But it have one bug in lower versions. It called multiple times
I found another solution
SomeView(buttonClicked:
Binding(get: { self.buttonClicked },
set: { self.buttonClicked = $0
print("your action here ")
}))

Related

iOS 16 Binding issue (in this case Toggle)

What i have stumped across is that when i pass a Binding<> as a parameter in a ViewModel as you will see below, it doesn't always work as intended. It works in iOS 15 but not in iOS 16. This reduces the capability to create subviews to handle this without sending the entire Holder object as a #Binding down to it's subview, which i would say would be bad practice.
So i have tried to minimize this as much as possible and here is my leasy amount of code to reproduce it:
import SwiftUI
enum Loadable<T> {
case loaded(T)
case notRequested
}
struct SuperBinder {
let loadable: Binding<Loadable<ContentView.ViewModel>>
let toggler: Binding<Bool>
}
struct Holder {
var loadable: Loadable<ContentView.ViewModel> = .notRequested
var toggler: Bool = false
func generateContent(superBinder: SuperBinder) {
superBinder.loadable.wrappedValue = .loaded(.init(toggleBinder: superBinder.toggler))
}
}
struct ContentView: View {
#State var holder = Holder()
var superBinder: SuperBinder {
.init(loadable: $holder.loadable, toggler: $holder.toggler)
}
var body: some View {
switch holder.loadable {
case .notRequested:
Text("")
.onAppear(perform: {
holder.generateContent(superBinder: superBinder)
})
case .loaded(let viewModel):
Toggle("testA", isOn: viewModel.toggleBinder) // Works but doesn't update UI
Toggle("testB", isOn: $holder.toggler) // Works completly
// Pressing TestA will even toggle TestB
// TestB can be turned of directly but has to be pressed twice to turn on. Or something?
}
}
struct ViewModel {
let toggleBinder: Binding<Bool>
}
}
anyone else stumbled across the same problem? Or do i not understand how binders work?

swiftUI modify state var from remote closure

I've got an application written in swift/swiftUI.
the logic part composed of events generator that enable the client to register a callback for each event before the UI part starts.
init.swift
-----------
func applicationDidFinishLaunching(_ aNotification: Notification) {
let event1Handler : eventHandler = {(data) in
ContentView().alert_handler.showAlert = true
}
let event2Handler : eventHandler = {(data) in
...
}
eventGenerator.sharedInstance().registerUIEvent1(event1Handler, event2: event2Handler)
window = NSWindow(...)
window.contentView = NSHostingView(rootView: ContentView())
...
}
in the UI part, there's an optional alert that is presented depent on the showAlert that can be set from the closure from the previous file...
class alertHandler: ObservableObject {
#Published var showAlert = false
}
struct ContentView: View {
#ObservedObject var alert_handler = alertHandler()
var body: some View {
GeometryReader { metrics in
.....
}.alert(isPresented: $alert_handler.showAlert, content: {
Alert(title: Text("Alert:"),
message: Text("press OK to execute default action..."),
dismissButton: Alert.Button.default(
Text("Press ok here"), action: { print("Hello world!") }
)
)
})
Unfortunately I couldn't see the alert appears when the first event was triggered. Perhaps anyone can tell me if I'd doing anything wrong ? or suggest an alternative approach to modify swiftUI struct variable (event_handler) from remote closure ?
I believe that my the problem may derived from ContentView module which is not a singleton, so when I set showAlert I'm doing so for another instance of ContentView. How can I fix my code to access showAlert that belongs to the currently running instance of ContentView ?
Your suspicion about ContentView not being a singleton instance is correct. You can solve this by owning your alertHandler in the parent (in this case, the app delegate) and passing that down to ContentView.
var handler = alertHandler()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let event1Handler : eventHandler = { (data) in
self.handler.showAlert = true //<-- Here
}
let event2Handler : eventHandler = {(data) in
...
}
eventGenerator.sharedInstance().registerUIEvent1(event1Handler, event2: event2Handler)
window = NSWindow(...)
window.contentView = NSHostingView(rootView: ContentView(alert_handler: handler))
...
}
struct ContentView: View {
#ObservedObject var alert_handler : alertHandler //<-- Here
That way, when you modify the showAlert property, the ContentView gets updated because it's the same instance of alertHandler.
Note: I'd consider adopting the Swift conventions of capitalizing type names and using camel case rather than snake case -- it'll make it easier for others to read your code.

How do I call a function on a view when the #ObservedObject is updated

I have an Observed Object that's working properly. When I update a String, the label updates.
However, I have a Bool that needs to call a custom function when it changes inside of an Observable Object. When the Bool is set to true, I need to flash the background color for 0.1 seconds.
class Event: ObservableObject {
static let current = Event()
#Published var name = ""
#Published var pass = false
}
struct EnterDistanceView: View {
#ObservedObject var event = Event.current
//when event.pass == true, call this
func flash() {
//UI update of flash
}
}
How do I call a method when a property inside of the #ObservedObject changes? Is this possible, or do I need to create
You can use onReceive to do imperative actions when a published value changes:
struct EnterDistanceView: View {
#ObservedObject var event = Event.current
func flash() {
//UI update of flash
}
var body: some View {
Text("Hello world")
.onReceive(event.$pass) { value in
if value {
//do your imperitive code here
flash()
}
}
}
}
Inside your flash method, I assume you'll want to change the value of a #State variable representing the screen color and then use DispatchQueue.main.asyncAfter to change it back shortly thereafter.

SwiftUI #FocusState - how to give it initial value

I am excited to see the TextField enhancement: focused(...): https://developer.apple.com/documentation/swiftui/view/focused(_:)
I want to use it to show a very simple SwitfUI view that contains only one TextField that has the focus with keyboard open immediately. Not able to get it work:
struct EditTextView: View {
#FocusState private var isFocused: Bool
#State private var name = "test"
// ...
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Enter your name", text: $name).focused($isFocused)
.onAppear {
isFocused = true
}
// ...
Anything wrong? I have trouble to give it default value.
I was also not able to get this work on Xcode 13, beta 5. To fix, I delayed the call to isFocused = true. That worked!
The theory I have behind the bug is that at the time of onAppear the TextField is not ready to become first responder, so isFocused = true and iOS calls becomeFirstResponder behind the scenes, but it fails (ex. the view hierarchy is not yet done setting up).
struct MyView: View {
#State var text: String
#FocusState private var isFocused: Bool
var body: some View {
Form {
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: isFocused) { isFocused in
// this will get called after the delay
}
.onAppear {
// key part: delay setting isFocused until after some-internal-iOS setup
DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
isFocused = true
}
}
}
}
}
I was also not able to get this work on Xcode 13, beta 5. To fix, I delayed the call to isFocused = true. That worked!
It also works without delay.
DispatchQueue.main.async {
isFocused = true
}
//This work in iOS 15.You can try it.
struct ContentView: View {
#FocusState private var isFocused: Bool
#State private var username = "Test"
var body: some View {
VStack {
TextField("Enter your username", text: $username)
.focused($isFocused).onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isFocused = true
}
}
}
}
}
I've had success adding the onAppear to the outermost view (in your case NavigationView):
struct EditTextView: View {
#FocusState private var isFocused: Bool
#State private var name = "test"
// ...
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Enter your name", text: $name).focused($isFocused)
}
}
}
.onAppear {
isFocused = true
}
}
// ...
I’m not certain but perhaps your onAppear attached to the TextField isn’t running. I would suggest adding a print inside of the onAppear to confirm the code is executing.
I faced the same problem and had the idea to solve it by embedding a UIViewController so could use viewDidAppear. Here is a working example:
import SwiftUI
import UIKit
struct FocusTestView : View {
#State var presented = false
var body: some View {
Button("Click Me") {
presented = true
}
.sheet(isPresented: $presented) {
LoginForm()
}
}
}
struct LoginForm : View {
enum Field: Hashable {
case usernameField
case passwordField
}
#State private var username = ""
#State private var password = ""
#FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
// handleLogin(username, password)
}
}
}
.uiKitOnAppear {
focusedField = .usernameField
// If your form appears multiple times you might want to check other values before setting the focus.
}
}
}
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.action = action
return vc
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
}
}
class UIAppearViewController: UIViewController {
var action: () -> Void = {}
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
// had to delay the action to make it work.
DispatchQueue.main.asyncAfter(deadline:.now()) { [weak self] in
self?.action()
}
}
}
public extension View {
func uiKitOnAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}
UIKitAppear was taken from this dev forum post, modified with dispatch async to call the action. LoginForm is from the docs on FocusState with the uiKitOnAppear modifier added to set the initial focus state.
It could perhaps be improved by using a first responder method of the VC rather than the didAppear, then perhaps the dispatch async could be avoided.

Is there a SwiftUI equivalent to WKInterfaceController's .becomeCurrentPage method?

Previously in WatchKit we could tell a certain InterfaceController to present itself using .becomeCurrentPage, how can we do it in Swift UI?
In WatchKit for example I would:
// handle notification
#objc func respondToWaterlock(_ notification: NSNotification) {
self.becomeCurrentPage()
}
there is no equivalent for becomeCurrentPage method in SwiftUI. You can update your State or ViewModel to achieve a similar result.
for example:
enum Pages {
case home
case settings
}
struct MyView: View {
#State var selectedPage: Pages = .home
var body: some View {
Group {
if self.selectedPage == .home {
Text("Home")
} else if self.selectedPage == .settings {
Text("Settings")
}
}
}
}
You should just update the selectedPage state variable to change the page.

Resources