SwiftUI TextField loses focus when styled - swift

I'm trying to apply some validation to a TextField to add a red border around the field when the content is invalid (in this case, I'm validating that the content is a positive number that is less than the specified maxLength).
The validation works fine and the border is applied when the value is out of range. However, when the border is applied to the TextField, the TextField loses focus in the UI (and also loses focus when the border is removed).
Here is a snippet of my code (I've included some extensions I'm using, but I don't think those are relevant to the issue)
import Foundation
import SwiftUI
import Combine
struct MyView : View {
#Binding var value: Int
var label: String
var maxLength: Int
#State private var valid: Bool = true
var body: some View {
TextField(label, value: $value, format: .number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.fixedSize()
.multilineTextAlignment(.trailing)
.onReceive(Just(value), perform: validate)
.if(!valid, transform: { $0.border(.red)})
}
func validate(val: Int) {
let newVal = val.clamped(to: 0...maxLength)
if newVal != val {
valid = false
} else {
valid = true
}
}
}
extension View {
#ViewBuilder
func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition { transform(self) }
else { self }
}
}
extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}
Is there any way to preserve focus on the TextField when styles are conditionally applied to it?
Or am I approaching validation wrong altogether? Is there a better way to check fields and apply conditional styling?

Because of the way your if modifier is structured, SwiftUI is unable to see that the underlying View is the same in the two conditions. For more detail on this, I'd suggest you watch Demystifying SwiftUI from WWDC 2021.
One solution is the simplify your border modifier into the following:
.border(valid ? .clear : .red)
This way, SwiftUI can still tell that this is the same underlying View.

Related

Can we use Bool for animatableData in SwiftUI?

I can animate my shape with updating a CGFloat value type simply, now I was looking to more reflector in my code to make it working with Bool Type as well, here what I have tried:
struct ContentView: View {
#State private var show: Bool = true
var body: some View {
MyShape(show: show)
.stroke()
.animation(.default, value: show)
Button("show") { show.toggle() }
}
}
struct MyShape: Shape {
var show: Bool
var animatableData: Bool {
get { return show }
set(newValue) { show = newValue }
}
func path(in rect: CGRect) -> Path {
return Path { path in
path.addLines([CGPoint(x: 0, y: 100), CGPoint(x: show ? 200 : 0, y: 100)])
}
}
}
my working code version was using Bool value in initialization for the length of line and then my animatableData was the length of line, and that would work, as I mentioned looking to use just Bool and animate the Shape just with bool, currently my code does not animate, looking to use just Bool for making animation happen. Can we do this? or I must use a helper value type like CGFloat to translate true/false to CGFloat. But I am NOT looking for an answer using Bool and a helper value Type, I already now that way.
You can't use Bool directly as your animatableData because Bool doesn't implement VectorArithmetic. There isn't really a sensible implementation of VectorArithmetic for Bool. How would you implement scale(by:)?
AnimatableData requires VectorArithmetic so that SwiftUI can smoothly interpolate between two values of animatableData without knowing anything else about the animatableData. There's no way to smoothly interpolate between false and true.

SwiftUI: using view modifiers between different iOS versions without #available

I use the following code snippet (in Xcode 13 Beta 5 and deployment target set to 14.0) to apply view modifiers conditionally according to iOS version:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.modifyFor(iOS14: {
$0.onAppear {
//do some stuff
}
}, iOS15: {
$0.task { //<---- Error: 'task(priority:_:)' is only available in iOS 15.0 or newer
//do some stuff
}
})
}
}
struct CompatibleView<Input: View,
Output14: View,
Output15: View>: View {
var content: Input
var iOS14modifier: ((Input) -> Output14)?
var iOS15modifier: ((Input) -> Output15)?
#ViewBuilder var body: some View {
if #available(iOS 15, *) {
if let modifier = iOS15modifier {
modifier(content)
}
else { content }
}
else {
if let modifier = iOS14modifier {
modifier(content)
}
else { content }
}
}
}
extension View {
func modifyFor<T: View, U: View>(iOS14: ((Self) -> T)? = nil,
iOS15: ((Self) -> U)? = nil) -> some View {
CompatibleView(content: self,
iOS14modifier: iOS14,
iOS15modifier: iOS15)
}
}
this code works great as long as I don't use iOS 15's view modifiers, but if I want to use any of those modifiers (like Task for ex.) then I need to use the #available directive which's an option I don't wanna opt in, because my codebase is large, there are many parts that should adopt the new iOS 15 modifiers and by using #available everywhere in the code will make it looks like a dish of Lasagna.
how to make this piece of code compiles in a clean way and without using the #available check ?
The best solution for so far I've figured out is to add simple modify extension function for view and use that.
It's useful if availability check for modifier is needed only in one place.
If needed in more than one place, then create new modifier function.
public extension View {
func modify<Content>(#ViewBuilder _ transform: (Self) -> Content) -> Content {
transform(self)
}
}
And using it would be:
Text("Good")
.modify {
if #available(iOS 15.0, *) {
$0.badge(2)
} else {
// Fallback on earlier versions
}
}
EDIT:
#ViewBuilder
func modify<Content: View>(#ViewBuilder _ transform: (Self) -> Content?) -> some View {
if let view = transform(self), !(view is EmptyView) {
view
} else {
self
}
}
This allows us not to define fallback if not required and the view will stay untouchable.
Text("Good")
.modify {
if #available(iOS 15.0, *) {
$0.badge(2)
}
}
There is no way to do this without 'if #available', but there is a way to structure it in a somewhat clean way.
Define your own View Modifier on a wrapper View:
struct Backport<Content> {
let content: Content
}
extension View {
var backport: Backport<Self> { Backport(content: self) }
}
extension Backport where Content: View {
#ViewBuilder func badge(_ count: Int) -> some View {
if #available(iOS 15, *) {
content.badge(count)
} else {
content
}
}
}
You can then use these like this:
TabView {
Color.yellow
.tabItem {
Label("Example", systemImage: "hand.raised")
}
.backport.badge(5)
}
Blog post about it:
Using iOS-15-only View modifiers in older iOS versions
You can create a simple extension on View with #ViewBuilder
fileprivate extension View {
#ViewBuilder
var tabBarTintColor: some View {
if #available(iOS 16, *) {
self.tint(.red)
} else {
self.accentColor(.red)
}
}
}
To use it just have it chained with your existing view
TabView()
.tabBarTintColor
There is no point because even if you did back-port a modifier named task (which is how this problem is normally solved) you won’t be able to use all the magic of async/await inside which is what it was designed for. If you have a good reason for not targeting iOS 15 (I don’t know any good ones) then just continue to use onAppear as normal and either standard dispatch queue async or Combine in an #StateObject.
There is no logical use case for that modifier for the issue you are trying to solve! You have no idea, how many times your app would check your condition about availability of iOS15 in each render! Maybe 1000 of times! Insane number of control which is totally bad idea! instead use deferent Views for each scenarios like this, it would checked just one time:
WindowGroup {
if #available(iOS 15, *) {
ContentView_For_iOS15()
}
else {
ContentView_For_No_iOS15()
}
}

How can I remove already rendered unneeded Views instead of rendering needed Views?

I am working on a CustomForEach which would act and work like a normal ForEach in SwiftUI, this CustomForEach has it own early days and it has some issues for use for me, which makes me to learn more about SwiftUI and challenge me to try to solve the issues, one of this issues is finding a way to destroy the unneeded Views instated of rendering all needed Views!
Currently when I update lowerBound the CustomForEach starts rendering for new range which is understandable. But the new range need less Views than before and that is not understandable to rendering them again for already rendered Views.
Goal: I want find a way to stop rendering all needed Views because they are already exist and there is no need to rendering again, and just removing the unneeded Views. And also I do not want start an another expensive calculation inside CustomForEach for finding out if the Views already exist!
struct TextView: View {
let string: String
var body: some View {
print("rendering " + string)
return HStack {
Text(string)
Circle().fill(Color.red).frame(width: 5, height: 5, alignment: .center)
}
}
}
struct CustomForEachView<Content: View>: View {
private let id: Int
let range: ClosedRange<Int>
let content: (Int) -> Content
init(range: ClosedRange<Int>, #ViewBuilder content: #escaping (Int) -> Content) {
self.id = range.lowerBound
self.range = range
self.content = content
}
// The issue is rendering all existed Views when lower Bound get updated, even we do not need to render new View in updating lower Bound!
var body: some View {
content(range.lowerBound)
if let suffixRange = suffix(of: range) {
CustomForEachView(range: suffixRange, content: content)
}
}
private func suffix(of range: ClosedRange<Int>) -> ClosedRange<Int>? {
return (range.count > 1) ? (range.lowerBound + 1)...range.upperBound : nil
}
}
struct ContentView: View {
#State private var lowerBound: Int = -2
#State private var upperBound: Int = 2
var body: some View {
HStack {
CustomForEachView(range: lowerBound...upperBound) { item in
TextView(string: item.description)
}
}
HStack {
Button("add lowerBound") { lowerBound += 1 }
Spacer()
Button("add upperBound") { upperBound += 1 }
}
.padding()
}
}
First of all, one thing important thing to understand is that a SwiftUI.View struct is not a view instance that is rendered on the screen. It's merely a description of the desired view hierarchy. The SwiftUI.View instances are going to be recreated and torn down a lot by the framework anyway.
The SwiftUI framework takes care of the actual rendering. It might use UIViews for this, or it might not. That's an implementation detail you shouldn't need to worry about in most cases.
That said, you might be able to help the framework by adding explicit ids to the views by using the id modifier. That way SwiftUI can use that to keep track of which view is which.
But, I'm not sure if that would actually help. Just an idea.

SwiftUI - How to disable sidebar from collapsing?

Gif to understand easier
Is there any way to disable collapsibility of SidebarListStyle NavigationViews?
EDIT: This method still works as of late 2022, and has never stopped working on any version of macOS (up to latest Ventura 13.1). Not sure why there are answers here suggesting otherwise. If the Introspection library changes their API you may need to update your calls accordingly, but the gist of the solution is the same.
Using this SwiftUI Introspection library:
https://github.com/siteline/SwiftUI-Introspect
We can introspect the underlying NSSplitView by extending their functionality:
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return introspect(selector: TargetViewSelector.ancestorOrSibling, customize: customize)
}
And then create a generic extension on View:
public extension View {
func preventSidebarCollapse() -> some View {
return introspectSplitView { splitView in
(splitView.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
}
}
Which can be used on our sidebar:
var body: some View {
(...)
MySidebar()
.preventSidebarCollapse()
}
The introspection library mentioned by Oskar is not working for MacOS.
Inspired by that, I figured out a solution for MacOS.
The rationality behind the solution is to use a subtle way to find out the parent view of a NavigationView which is a NSSplitViewController in the current window.
Below codes was tested on XCode 13.2 and macOS 12.1.
var body: some View {
Text("Replace with your sidebar view")
.onAppear {
guard let nsSplitView = findNSSplitVIew(view: NSApp.windows.first?.contentView), let controller = nsSplitView.delegate as? NSSplitViewController else {
return
}
controller.splitViewItems.first?.canCollapse = false
// set the width of your side bar here.
controller.splitViewItems.first?.minimumThickness = 150
controller.splitViewItems.first?.maximumThickness = 150
}
}
private func findNSSplitVIew(view: NSView?) -> NSSplitView? {
var queue = [NSView]()
if let root = view {
queue.append(root)
}
while !queue.isEmpty {
let current = queue.removeFirst()
if current is NSSplitView {
return current as? NSSplitView
}
for subview in current.subviews {
queue.append(subview)
}
}
return nil
}
While the method that Oskar used with the Introspect library no longer works, I did find another way of preventing the sidebar from collapsing using Introspect. First, you need to make an extension on View:
extension View {
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return inject(AppKitIntrospectionView(
selector: { introspectionView in
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
return nil
}
return Introspect.findAncestorOrAncestorChild(ofType: NSSplitView.self, from: viewHost)
},
customize: customize
))
}
}
Then do the following:
NavigationView {
SidebarView()
.introspectSplitView { controller in
(controller.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
Text("Main View")
}
This being said, we don't know how long this will actually work for. Apple could change how NavigationView works and this method may stop working in the future.

SwiftUI: how to play ping-pong animation once? Correct way to play animation forward and backward?

Sample of what I need:
.
As there is absent .onAnimationCompleted { // Some work... } its pretty problematic.
Generally I need the solution that will have a following characteristics:
Most short and elegant way of playing some ping-pong animation ONCE. Not infinite!
Make code reusable. As example - made it as ViewModifier.
To have a way to call animation externally
my code:
import SwiftUI
import Combine
struct ContentView: View {
#State var descr: String = ""
#State var onError = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
BlurredTextField(title: "Description", text: $descr, onError: $onError)
Button("Commit") {
if self.descr.isEmpty {
self.onError.send()
}
}
}
}
}
struct BlurredTextField: View {
let title: String
#Binding var text: String
#Binding var onError: PassthroughSubject<Void, Never>
#State private var anim: Bool = false
#State private var timer: Timer?
#State private var cancellables: Set<AnyCancellable> = Set()
private let animationDiration: Double = 1
var body: some View {
TextField(title, text: $text)
.blur(radius: anim ? 10 : 0)
.animation(.easeInOut(duration: animationDiration))
.onAppear {
self.onError
.sink(receiveValue: self.toggleError)
.store(in: &self.cancellables)
}
}
func toggleError() {
timer?.invalidate()// no blinking hack
anim = true
timer = Timer.scheduledTimer(withTimeInterval: animationDiration, repeats: false) { _ in
self.anim = false
}
}
}
How about this? Nice call site, logic encapsulated away from your main view, optional blink duration. All you need to provide is the PassthroughSubject, and call .send() when you want the blink to happen.
import SwiftUI
import Combine
struct ContentView: View {
let blinkPublisher = PassthroughSubject<Void, Never>()
var body: some View {
VStack(spacing: 10) {
Button("Blink") {
self.blinkPublisher.send()
}
Text("Hi")
.addOpacityBlinker(subscribedTo: blinkPublisher)
Text("Hi")
.addOpacityBlinker(subscribedTo: blinkPublisher, duration: 0.5)
}
}
}
Here's the view extension you would call
extension View {
// the generic constraints here tell the compiler to accept any publisher
// that sends outputs no value and never errors
// this could be a PassthroughSubject like above, or we could even set up a TimerPublisher
// that publishes on an interval, if we wanted a looping animation
// (we'd have to map it's output to Void first)
func addOpacityBlinker<T: Publisher>(subscribedTo publisher: T, duration: Double = 1)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.modifier(OpacityBlinker(subscribedTo: publisher.eraseToAnyPublisher(),
duration: duration))
}
}
Here's the ViewModifier where the magic actually happens
// you could call the .modifier(OpacityBlinker(...)) on your view directly,
// but I like the View extension method, as it just feels cleaner to me
struct OpacityBlinker: ViewModifier {
// this is just here to switch on and off, animating the blur on and off
#State private var isBlurred = false
var publisher: AnyPublisher<Void, Never>
// The total time it takes to blur and unblur
var duration: Double
// this initializer is not necessary, but allows us to specify a default value for duration,
// and the call side looks nicer with the 'subscribedTo' label
init(subscribedTo publisher: AnyPublisher<Void, Never>, duration: Double = 1) {
self.publisher = publisher
self.duration = duration
}
func body(content: Content) -> some View {
content
.blur(radius: isBlurred ? 10 : 0)
// This basically subscribes to the publisher, and triggers the closure
// whenever the publisher fires
.onReceive(publisher) { _ in
// perform the first half of the animation by changing isBlurred to true
// this takes place over half the duration
withAnimation(.linear(duration: self.duration / 2)) {
self.isBlurred = true
// schedule isBlurred to return to false after half the duration
// this means that the end state will return to an unblurred view
DispatchQueue.main.asyncAfter(deadline: .now() + self.duration / 2) {
withAnimation(.linear(duration: self.duration / 2)) {
self.isBlurred = false
}
}
}
}
}
}
John's answer is absolutely great and helped me get to exactly what I was looking for. I extended the answer to allow for any view modification to "flash" once and return.
Example Result:
Example Code:
struct FlashTestView : View {
let flashPublisher1 = PassthroughSubject<Void, Never>()
let flashPublisher2 = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Text("Scale Out & In")
.padding(20)
.background(Color.white)
.flash(on: flashPublisher1) { (view, isFlashing) in
view
.scaleEffect(isFlashing ? 1.5 : 1)
}
.onTapGesture {
flashPublisher1.send()
}
Divider()
Text("Flash Text & Background")
.padding(20)
// Connivence view extension for background and text color
.flash(
on: flashPublisher2,
originalBackgroundColor: .white,
flashBackgroundColor: .blue,
originalForegroundColor: .primary,
flashForegroundColor: .white)
.onTapGesture {
flashPublisher2.send()
}
}
}
}
Here's the modified code from John's answer.
extension View {
/// Listens to a signal from a publisher and temporarily applies styles via the content callback.
/// - Parameters:
/// - publisher: The publisher that sends a signal to apply the temp styles.
/// - animation: The animation used to change properties.
/// - delayBack: How long, in seconds, after flashing starts should the styles start to revert. Typically this is the same duration as the animation.
/// - content: A closure with two arguments to allow customizing the view when flashing. Should return the modified view back out.
/// - view: The view being modified.
/// - isFlashing: A boolean to indicate if a flash should be applied. Example: `view.scaleEffect(isFlashing ? 1.5 : 1)`
/// - Returns: A view that applies its flash changes when it receives its signal.
func flash<T: Publisher, InnerContent: View>(
on publisher: T,
animation: Animation = .easeInOut(duration: 0.3),
delayBack: Double = 0.3,
#ViewBuilder content: #escaping (_ view: Self, _ isFlashing: Bool) -> InnerContent)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.modifier(
FlashStyleModifier(
publisher: publisher.eraseToAnyPublisher(),
animation: animation,
delayBack: delayBack,
content: { (view, isFlashing) in
return content(self, isFlashing)
}))
}
/// A helper function built on top of the method above.
/// Listens to a signal from a publisher and temporarily animates to a background color and text color.
/// - Parameters:
/// - publisher: The publisher that sends a signal to apply the temp styles.
/// - animation: The animation used to change properties.
/// - delayBack: How long, in seconds, after flashing starts should the styles start to revert. Typically this is the same duration as the animation.
/// - originalBackgroundColor: The normal state background color
/// - flashBackgroundColor: The background color when flashing.
/// - originalForegroundColor: The normal text color.
/// - flashForegroundColor: The text color when flashing.
/// - Returns: A view that flashes it's background and text color.
func flash<T: Publisher>(
on publisher: T,
animation: Animation = .easeInOut(duration: 0.3),
delayBack: Double = 0.3,
originalBackgroundColor: Color,
flashBackgroundColor: Color,
originalForegroundColor: Color,
flashForegroundColor: Color)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.flash(on: publisher, animation: animation) { view, isFlashing in
return view
// Need to apply arbitrary foreground color, but it's not animatable but need for colorMultiply to work.
.foregroundColor(.white)
// colorMultiply is animatable, so make foregroundColor flash happen here
.colorMultiply(isFlashing ? flashForegroundColor : originalForegroundColor)
// Apply background AFTER colorMultiply so that background color is not unexpectedly modified
.background(isFlashing ? flashBackgroundColor : originalBackgroundColor)
}
}
}
/// A view modifier that temporarily applies styles based on a signal from a publisher.
struct FlashStyleModifier<InnerContent: View>: ViewModifier {
#State
private var isFlashing = false
let publisher: AnyPublisher<Void, Never>
let animation: Animation
let delayBack: Double
let content: (_ view: Content, _ isFlashing: Bool) -> InnerContent
func body(content: Content) -> some View {
self.content(content, isFlashing)
.onReceive(publisher) { _ in
withAnimation(animation) {
self.isFlashing = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + delayBack) {
withAnimation(animation) {
self.isFlashing = false
}
}
}
}
}