Using #FocusState on a TextField causing memory leaks - ios

I am learning memory and ARC since a while and managed to use the Leaks instrument more often in order to produce quality code. Having that said, please consider my lack of experience in this zone.
Problem: I built a parent view A that presents a view B.
B contains a login form built using a TextField, a SecureField and a Button. I also have a #FocusState private var isFocused: Bool property that helps me hide the keyboard in order to bypass the "AttributeGraph: cycle detected" console error that shows up once I disable the textfield on button press having the keyboard on screen. (Get `AttributeGraph: cycle detected` error when changing disabled state of text field)
I noticed that when I use the #FocusState property, once I dismiss B, the Leaks instrument detects two "Malloc 32 Bytes" leaks just like in the picture below.
If I don't use the #FocusState property, the leaks will no longer show up. Am I doing something wrong or is this some kind of bug / false positive from Swift?
This view is partially extracted from my file so it doesn't have all it's properties and methods here.
struct AuthenticationLoginView: View {
#StateObject var viewModel = AuthenticationLoginViewModel()
#FocusState private var isFocused: Bool
var body: some View {
VStack {
TextField(text: $viewModel.username) {
Text("placeholder.")
}
.tag(AuthenticationLoginField.username)
.textInputAutocapitalization(.never)
.focused($isFocused)
.disabled(viewModel.isLoggingIn)
SecureField(text: $viewModel.password) {
Text("Password")
}
// .focused($isFocused)
.disabled(viewModel.isLoggingIn)
.tag(AuthenticationLoginField.password)
}
}
}

Without more code it is hard to tell what else you are doing that may have caused the leak.
My gut is that having a single focus boolean when you have two edit fields is an anti-pattern compared to Apple's way. When something is evolving like SwiftUI, try to follow their example styles more closely. Use a boolean only when there's just one focusable field.
This similar Hacking with Swift sample shows using an optional and changing the focus as fields submitted.
#FocusState private var focusedField: FocusedField?
#State private var username = "Anonymous"
#State private var password = "sekrit"
var body: some View {
VStack {
TextField("Enter your username", text: $username)
.focused($focusedField, equals: .username)
SecureField("Enter your password", text: $password)
.focused($focusedField, equals: .password)
}
.onSubmit {
if focusedField == .username {
focusedField = .password
} else {
focusedField = nil
}
}

I found this problem. In my case, it caused a massive retain loop which fed all the way back to coordinator with a bunch of objects.
Just showing a TextField with focus (either the bool way, or the enum way) caused a retain.
My fix was to use the introspect library and just set first responder manually
#State private var textField:UITextField?
#State private var secureTextField:UITextField?
var body: some View {
ZStack {
TextField(prompt, text: $text)
.opacity(isSecure ? 0 : 1)
.introspectTextField { field in
textField = field
}
SecureField(prompt,text: $text)
.opacity(isSecure ? 1 : 0)
.introspectTextField { field in
secureTextField = field
}
}
.animation(.easeInOut, value: isSecure)
.overlay(alignment: .trailing) {
Button {
isSecure.toggle()
setFocus()
} label: {
Image(isSecure ? "eyeClose" : "eyeOpen")
}
.opacity(canBeSecure ? 1 : 0)
}
.onAppear {
DispatchQueue.main.async {
setFocus()
}
}
}
func setFocus() {
if isSecure {
secureTextField?.becomeFirstResponder()
}
else {
textField?.becomeFirstResponder()
}
}

Related

Presence of #Environment dismiss causes list to constantly rebuild its content on scrolling

I need to build a list of TextFields where each field is associated with focus id, so that I can auto scroll to such a text field when it receives focus. In reality the real app is a bit more complex which also includes TextEditors and many other controls.
Now, I found out that if my view defines #Environment(\.dismiss) private var dismiss then the list is rebuilding all the time during manual scrolling. If I just comment out the line #Environment(\.dismiss) private var dismiss then there is no rebuilding of the list when I scroll. Obviously, I want to be able to dismiss my view when user clicks some button. In the real app it's even worse: during scrolling everything is lagging, I cannot get smooth scrolling. And my list is not huge it's just 10 items or so.
Here is a demo example:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink {
DismissListView()
} label: {
Text("Go to see the list")
}
}
}
}
struct DismissListView: View {
#Environment(\.dismiss) private var dismiss
enum Field: Hashable {
case line(Int)
}
#FocusState private var focus: Field?
#State private var text: String = ""
var body: some View {
ScrollViewReader { proxy in
List {
let _ = print("body is rebuilding")
Button("Dismiss me") {
dismiss()
}
Section("Section") {
ForEach((1...100), id: \.self) {num in
TextField("text", text: $text)
.id(Field.line(num))
.focused($focus, equals: .line(num))
}
}
}
.listStyle(.insetGrouped)
.onChange(of: focus) {_ in
withAnimation {
proxy.scrollTo(focus, anchor: .center)
}
}
}
}
}
The questions are:
Why is the list rebuilding during manual back and forth scrolling when #Environment(\.dismiss) private var dismiss is defined, and the same is NOT happening when dismiss is NOT defined?
Is there any workaround for this: I need to be able to use ScrollProxyReader to focus any text field when the focus changes, and I need to be able to dismiss the view, but in the same time I need to avoid constant rebuilds of the list during scrolling, because it drops app performance and scrolling becomes jagged...
P.S. Demo app constantly outputs "body is rebuilding" when dismiss is defined and the list is scrolled, but if any text field gets a focus manually, then the "body is rebuilding" is not printed anymore even if the dismiss is still defined.
I could make an assumption, but that would be really rather a guess (based on experience, observations, etc). In a fact, all WHYs like "why this sh... (bug) happens" should be asked on https://developer.apple.com/forums/ (there are Apple's engineers there) or reported to https://developer.apple.com/bug-reporting/
A solution is to separate dismiss depenent part into dedicated view, so hiding it from parent body (and so do not affect it)
struct DismissView: View {
// visible only for this view !!
#Environment(\.dismiss) private var dismiss
var body: some View {
Button("Dismiss me") {
// affects current context, so it does not matter
// in which sub-view is called
dismiss()
}
}
}
var body: some View {
ScrollViewReader { proxy in
List {
let _ = print("body is rebuilding")
DismissView() // << here !!
// ... other code

SwiftUI replace and copy text TextEditor

I have read countless articles and watched 10 hours of YouTube videos and still can't figure this out, maybe I'm just missing the term. I'm old school and still trying to learn this backwards swift stuff compared to vb or javascript.
I am using the default Mac OS document template in Xcode 13.1. it comes with a dialogue to open a file then dumps it into the TextEditor as utf8 string.
For my part I added another TextEditor and a button but keep getting error when I try to copy the text from the original texteditor to the new texteditor when button is pressed.
struct ContentView: View {
#Binding var document: test2Document
#State var newtexteditor: String = "press the button"
var body: some View {
VStack{
//main text editor
TextEditor(text: $document.text)
//divider
Divider()
//new text editor
TextEditor(text: $newtexteditor).padding()
//button
Button("test", action: dosomething)
}
}
func dosomething () {
$newtexteditor = $document
}
}
Also, is there any ways of giving objects a name or label and using that to say things like textbox1.text = textbox2.text like you could using storyboards. I obviously know that's not swift syntax but you get the point.
#State and #Binding vars are special kinds of variables. They actually contain 2 sub-variables: projectedValue and wrappedValue.
projectedValue is like a reference to the actual value, and allows the #State/#Binding to be modified from further down the view hierarchy.
You access this by saying $newTextEditor or $document.text.
This is only used for passing references down the view hierarchy, where you can later set the wrappedValue.
wrappedValue is the actual value — in your case, a String.
You access this by just saying newTextEditor or document.text. No $.
You're able to set this - for example, newTextEditor = "New text" or newTextEditor = document.text. The UI will automatically update to reflect the changes.
Here's an example:
struct ContentView: View {
#State var text = "Hello"
var body: some View {
VStack {
Text("The text is: \(text)") /// display the `wrappedValue`
SubView(text: $text) /// pass in the `projectedValue`
}
}
}
struct SubView: View {
#Binding var text: String
var body: some View {
Button("Click me to change text") {
text = "New text" /// set the `wrappedValue`
}
}
}
In your code, it would look something like this:
struct ContentView: View {
#Binding var document: Test2Document /// Side Note: structs like `Test2Document` should be Capitalized
#State var newTextEditor: String = "press the button" /// also use camel case
var body: some View {
VStack{
TextEditor(text: $document.text) /// pass in the `projectedValue`
Divider()
TextEditor(text: $newTextEditor) /// pass in the `projectedValue`
.padding()
Button("test", action: dosomething)
}
}
func dosomething() {
newTextEditor = document.text /// set the `wrappedValue`
}
}

SwiftUI List animation causes the app to show outdated results when the binding changes quickly

I am trying to animate search changes in a list with SwiftUI 2 / Xcode 12, however the animation can't keep up when searching quickly. Originally this is within a project using a search bar through UIKit but I can reproduce this easily in SwiftUI alone:
struct ContentView: View {
#State private var searchText = ""
private var arrayToAnimate: [String] = ["aa", "aaa", "a", "fg", "fgh", "rd"]
private func filtededListByValue(_ filterValue: String) -> [String] {
return arrayToAnimate.filter { filterValue.isEmpty || $0.lowercased().contains(filterValue.lowercased()) }
}
var body: some View {
TextField("Search text", text: $searchText) // $searchText.animation() has the same problem
List {
ForEach(filtededListByValue(searchText), id: \.self) { item in
Text(item)
}
}.animation(.easeIn) // When typing fast this causes the list to display incorrect data
}
/*
// Using this instead has the exact same problem
private var searchBinding: Binding<String> {
Binding<String>(
get: { return self.searchText },
set: { newSearchText in
withAnimation {
self.searchText = newSearchText
}
}
)
}*/
}
When slowly typing the results will be correct but if I for example type aaa and back to nothing fast then not all items will show even though there is no search text. If I use the get/set version of the binding instead it has the same problem. How do we handle this in SwiftUI? Is it a bug in SwiftUI? If I remove the animation it works.
There really is no 'fix' for this. Your code is basically the most simplified example you can have for your goal. If you were making an api call it would be different. Try different animations, or using a completion handler for your filtering.
Closing this since I can't reproduce this issue with SwiftUI 3. I assume it was a bug with SwiftUI 2.

SwiftUI, how to bind EnvironmnetObject Int property to TextField?

I have an ObservableObject which is supposed to hold my application state:
final class Store: ObservableObject {
#Published var fetchInterval = 30
}
now, that object is being in injected at the root of my hierarchy and then at some component down the tree I'm trying to access it and bind it to a TextField, namely:
struct ConfigurationView: View {
#EnvironmnetObject var store: Store
var body: some View {
TextField("Fetch interval", $store.fetchInterval, formatter: NumberFormatter())
Text("\(store.fetchInterval)"
}
}
Even though the variable is binded (with $), the property is not being updated, the initial value is displayed correctly but when I change it, the textfield changes but the binding is not propagated
Related to the first question, is, how would I receive an event once the value is changed, I tried the following snippet, but nothing is getting fired (I assume because the textfield is not correctly binded...
$fetchInterval
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.sink { interval in
print("sink from my code \(interval)")
}
Any help is much appreciated.
Edit: I just discovered that for text variables, the binding works fine out of the box, ex:
// on store
#Published var testString = "ropo"
// on component
TextField("Ropo", text: $store.testString)
Text("\(store.testString)")
it is only on the int field that it does not update the variable correctly
Edit 2:
Ok I have just discovered that only changing the field is not enough, one has to press Enter for the change to propagate, which is not what I want, I want the changes to propagate every time the field is changed...
For anyone that is interested, this is te solution I ended up with:
TextField("Seconds", text: Binding(
get: { String(self.store.fetchInterval) },
set: { self.store.fetchInterval = Int($0.filter { "0123456789".contains($0) }) ?? self.store.fetchInterval }
))
There is a small delay when a non-valid character is added, but it is the cleanest solution that does not allow for invalid characters without having to reset the state of the TextField.
It also immediately commits changes without having to wait for user to press enter or without having to wait for the field to blur.
Do it like this and you don't even have to press enter. This would work with EnvironmentObject too, if you put Store() in SceneDelegate:
struct ContentView: View {
#ObservedObject var store = Store()
var body: some View {
VStack {
TextField("Fetch interval", text: $store.fetchInterval)
Text("\(store.fetchInterval)")
}
} }
Concerning your 2nd question: In SwiftUI a view gets always updated automatically if a variable in it changes.
how about a simple solution that works well on macos as well, like this:
import SwiftUI
final class Store: ObservableObject {
#Published var fetchInterval: Int = 30
}
struct ContentView: View {
#ObservedObject var store = Store()
var body: some View {
VStack{
TextField("Fetch interval", text: Binding<String>(
get: { String(format: "%d", self.store.fetchInterval) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.store.fetchInterval = value.intValue
}}))
Text("\(store.fetchInterval)").padding()
}
}
}

Prevent NavigationLink from processing state variable

I don't like my title, but it's the best I could think of. Basically, I have two Views: SearchInput and SearchResultList. The user enters a search term with SearchInput, taps "Go" and then sees the results of the search with SearchResultList. The code is:
struct SearchInput: View {
#State private var searchTerm: String = ""
var searchResult: [RTDocument] {
return RuntimeDataModel.shared.fetchRTDocuments(matching: self.searchTerm)
}
var body: some View {
NavigationView {
VStack {
TextField("Enter search term...", text: self.$searchTerm)
NavigationLink(destination: SearchResultList(
rtDocuments: self.searchResult
)) { Text("Go") } //NavigationLink
} //VStack
} //NavigationView
} //body
} //SearchInput
The issue I'm having is that every time the user enters a character in TextField, the bound-state variable searchTerm is updated which causes NavigationLink to reevaluate its destination -- which causes a Core Data fetch up in the computed searchResult variable.
I'd like the fetch to happen only once, when the user taps "Go". Is there a way to accomplish that?
This is expected behaviour, so in general you should redesign your SearchResultList, but this can help as a walkaround:
Declare a state on your input:
#State var shouldNavigate = false
Then separate the button form the navigation link.
Group {
Button("Go") {
self.shouldNavigate = true
}
NavigationLink(destination: Text("SearchResultList"),
isActive: $shouldNavigate) {
EmptyView()
}
}

Resources