SwiftUI ViewModifier - add kerning - swift

Is there a way to build a view modifier that applies custom font and fontSize, as the below working example, and have in the same modifier the possibility to add kerning as well?
struct labelTextModifier: ViewModifier {
var fontSize: CGFloat
func body(content: Content) -> some View {
content
.font(.custom(Constants.defaultLabelFontSFProDisplayThin, size: fontSize))
}
}
extension View {
func applyLabelFont(size: CGFloat) -> some View {
return self.modifier(labelTextModifier(fontSize: size))
}
}
The above works well, however i cannot figure it out how to add kerning to the modifier as well
tried
content
.kerning(4)
, but did not work.
Suggestions?

Alternate is to use Text-only modifier, like
extension Text {
func applyLabelFont(size: CGFloat, kerning: CGFloat = 4) -> Text {
self
.font(.custom(Constants.defaultLabelFontSFProDisplayThin, size: size))
.kerning(kerning)
}
}

Related

SwiftUI vertical axis TextFields collapses to nothing when .fixedSize() is applied

iOS 16 (finally) allowed us to specify an axis: in TextField, letting text entry span over multiple lines.
However, I don't want my text field to always fill the available horizontal space. It should fill the amount of space taken up by the text that has been entered into it. To do this, we can apply .fixedSize().
However, using this two things in conjunction causes the text field to completely collapse and take up no space. This bug (?) does not affect a horizontal-scrolling text field.
Is this basic behaviour simply broken, or is there an obtuse but valid reason these methods don't play nice?
This is very simple to replicate:
struct ContentView: View {
#State var enteredText: String = "Test Text"
var body: some View {
TextField("Testing", text: $enteredText, axis: .vertical)
.padding()
.fixedSize()
.border(.red)
}
}
Running this will produce a red box the size of your padding. No text is shown.
I don't want my text field to always fill the available horizontal space. It should fill the amount of space taken up by the text that has been entered into it.
That's a weird wish. If you want to remove the background of the TextField, then do it. But I don't think it's a good idea to have an autosizing TextField. One of the reasons against it is the fact that if you erase all the text then the TextField will collapse to the zero width and you'll never set the cursor into it.
I had exactly the same problem with multiline text. So far the use of axis:.vertical requires a fixed width for the text field. This was for me a major problem when designing a table view where the column width adapts to the widest text field.
I found a very good working solution which I summarised in the following ViewModifier :
struct DynamicMultiLineTextField: ViewModifier {
let minWidth: CGFloat
let maxWidth: CGFloat
let font: UIFont
let text: String
var sizeOfText : CGSize {
get {
let font = self.font
let stringValue = self.text
let attributedString = NSAttributedString(string: stringValue, attributes: [.font: font])
let mySize = attributedString.size()
return CGSize(width: min(self.maxWidth, max(self.minWidth, mySize.width)), height: mySize.height)
}
}
func body(content: Content) -> some View {
content
.frame(minWidth: self.minWidth, idealWidth: self.sizeOfText.width ,maxWidth: self.maxWidth)
}
}
extension View {
func scrollableDynamicWidth(minWidth: CGFloat, maxWidth: CGFloat, font: UIFont, text: String) -> some View {
self.modifier(DynamicMultiLineTextField(minWidth: minWidth, maxWidth: maxWidth, font: font, text: text))
}
Usage (only on a TextField with the option: axis:.vertical):
TextField("Content", text:self.$tableCell.value, axis: .vertical)
.scrollableDynamicWidth(minWidth: 100, maxWidth: 800, font: self.tableCellFont, text: self.tableCell.value)
The text field width changes as you type. If you want to limit the length of a line type "option-return" which starts a new line.
On macOS the situation seems to be a bit more complicated. The problem seems to be that TextField does not extent its rendering surface beyond its initial size. So - when the field grows the text is invisible because not rendered.
I am using the following ViewModifier to force a larger rendering surface. I fear this can be called a "hack":
// For a scrollable TextField
struct DynamicMultiLineTextField: ViewModifier {
let minWidth: CGFloat
let maxWidth: CGFloat
let font: NSFont
#Binding var text: String
#FocusState var isFocused: Bool
#State var firstActivation : Bool = true
#State var backgroundFieldSize: CGSize? = nil
var fieldSize : CGSize {
get {
if let theSize = backgroundFieldSize {
return theSize
}
else {
return self.sizeOfText()
}
}
}
func sizeOfText() -> CGSize {
let font = self.font
let stringValue = self.text
let attributedString = NSAttributedString(string: stringValue, attributes: [.font: font])
let mySize = attributedString.size()
let theSize = CGSize(width: min(self.maxWidth, max(self.minWidth, mySize.width + 5)), height: mySize.height)
return theSize
}
func body(content: Content) -> some View {
content
.frame(width:self.fieldSize.width)
.focused(self.$isFocused)
.onChange(of: self.isFocused, perform: { value in
if value && self.firstActivation {
let oldText = self.text
self.backgroundFieldSize = CGSize(width:self.maxWidth, height:self.sizeOfText().height)
Task() {#MainActor () -> Void in
self.text = "nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text"
self.firstActivation = false
self.isFocused = false
}
Task() {#MainActor () -> Void in
self.text = oldText
try? await Task.sleep(nanoseconds: 1_000)
self.isFocused = true
self.backgroundFieldSize = nil
Task () {
self.firstActivation = true
}
}
}
})
}
}
extension View {
func scrollableDynamicWidth(minWidth: CGFloat, maxWidth: CGFloat, font: NSFont, text: Binding<String>) -> some View {
self.modifier(DynamicMultiLineTextField(minWidth: minWidth, maxWidth: maxWidth, font: font, text:text))
}
}
Usage:
TextField("Content", text:self.$tableCell.value, axis:.vertical)
.scrollableDynamicWidth(minWidth: 100, maxWidth: 800, font: self.tableCellFont, text: self.$tableCell.value)

SwiftUI TextField loses focus when styled

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.

ViewModifier to change both font and tracking

I'm trying to change the Font and the tracking of a text in SwiftUI.
So far I have created an extension for Text that sets the tracking.
extension Text {
func setFont(as font: Font.MyFonts) -> Self {
self.tracking(font.tracking)
}
}
I have also created a View Modifier that sets the correct font from my enum
extension Text {
func font(_ font: Font.MyFonts) -> some View {
ModifiedContent(content: self, modifier: MyFont(font: font))
}
}
struct MyFont: ViewModifier {
let font: Font.MyFonts
func body(content: Content) -> some View {
content
.font(.custom(font: font))
}
}
static func custom(font: MyFonts) -> Font {
return Font(font.font as CTFont)
}
I can't seem to find any way to combine these, since the view modifier returns some View and the tracking can only be set on a Text. Is there any clever way to combine these so I can only set the view Modifier?
the enum of fonts look like this
extension Font {
enum MyFonts {
case huge
case large
case medium
/// Custom fonts according to design specs
var font: UIFont {
var font: UIFont?
switch self {
case .huge: font = UIFont(name: AppFontName.book, size: 40)
case .large: font = UIFont(name: AppFontName.book, size: 28
case .medium: font = UIFont(name: AppFontName.book_cursive, size: 18)
}
return font ?? UIFont.systemFont(ofSize: 16)
}
var tracking: Double {
switch self {
case .huge:
return -0.25
default:
return 0
}
}
}
This is the app font name struct that I'm using
public struct AppFontName {
static let book = "Any custom font name"
static let book_cursive = "any custom font name cursive"
}
I still have errors for missed .custom, but anyway seems the solution for your code is to use own Text.font instead of View.font, like
extension Text {
// func font(_ font: Font.MyFonts) -> some View {
// ModifiedContent(content: self, modifier: MyFont(font: font))
// }
func font(_ font: Font.MyFonts) -> Self {
self.font(Font.custom(font: font))
}
}

How some functions can self provide needed parameters to work in SwiftUI?

There are some kind of protocols in SwiftUI which they have a function, that struct should have same function to be able to conform to that protocol,
like ViewModifier protocol which need
func body(content: Content) -> some View { return content }
or Shape protocol which need
func path(in rect: CGRect) -> Path { return Path { path in } }
When we are using those struct that conform to these protocols, we actually does not provide any parameters to those required functions from protocol, they grab there need parameters magically from context.
Which this auto grab parameters is big question for me how this process happens?
There for I re created Shape protocol to actually try and test this auto grab feature! How can my function automatically grab needed parameters! Right now my code is not grabbing need data, instead of returning Shape, it returns View! which I did all the things the same like apple done! why Shape protocol of apple returns Shape but my protocol does not, also my functions does not auto grab the needed data.
import SwiftUI
struct ContentView: View {
var body: some View {
CustomShape() // print: CustomShape: (0.0, 0.0, 414.0, 405.0) without body of CustomShape: Shape!
CustomSHAPE() // it does not print! But also returns unwished View!
}
}
struct CustomShape: Shape {
//var body: some View { Color.blue } // if we have body, path would stop working!
func path(in rect: CGRect) -> Path {
print("CustomShape:", rect)
return Path { path in }
}
}
protocol SHAPE: Animatable, View { }
struct CustomSHAPE: SHAPE {
var body: some View { Color.red } // if we have body, path would stop working! also I cannot comment it like we can in CustomShape: Shape
func path(in rect: CGRect) -> Path {
print("CustomSHAPE:", rect)
return Path { path in }
}
}
I think the problem comes down thinking in terms of "auto grabbing parameters."
The Shape protocol is where path(in:) is declared for SwiftUI. View "knows" that any type conforming to Shape provides a path(in:) method that it can call with the bounds of the view into which it wants to render the shape.
Your CustomShape isn't auto-grabbing anything. It conforms to Shape so View calls its path(in:). Under the hood, hidden away from from us mere mortals who don't work at Apple, there is an NSView or UIView that View is trying to render into. After figuring out how big that underlying AppKit/UIKit view has to be, and where it is to be positioned, (ie, after applying its layout constraints), it simply passes its bounds to CustomShape's path(in:) to get a Path, which I'm sure is ultimately just a struct wrapper for a CGPath.
Your CustomSHAPE on the other hand does not conform to Shape. It conforms to a different protocol, SHAPE. View doesn't know anything about SHAPE, so it can't do anything with it. All it knows is that it conforms to View, so it has to restrict what it does with CustomSHAPE to only the things the View protocol guarantees.
Basically your custom shapes need to conform to SwiftUI's protocols for SwiftUI to know how to use them.
Now, if you write your own generic View that wraps SHAPE instances, maybe you could implement your own forwarding to SHAPE's path(in:) method, but I'm not sure off the top of my head how to implement it in a way that properly hooks into SwiftUI's rendering.
Generally speaking, you don't need magic for your protocol to be able to "auto grab" stuff. You can set default values for a protocol's declarations by using extensions. Here is a simple example:
An example without "auto grab":
protocol MyProtocol {
func doSomething() -> Bool
}
// ERRORRRR! MyStructure doesnt conform to MyProtocol because you havent defined
// the required function `func doSomething() -> Bool`
struct MyStructure: MyProtocol {
}
An example with "auto grab":
protocol MyProtocol {
func doSomething() -> Bool
}
extension MyProtocol {
func doSomething() -> Bool {
print("Im doing all i can!")
return false
}
}
// No Errors because `func doSomething() -> Bool` has defaulted to the func
// that we declared in `extension MyProtocol`. so it "auto grab" the default func.
struct MyStructure: MyProtocol {
}
Remember this was only a simple example. You can do much more complicated "auto grab"s using the power of extensions.
This is almost what is happening in Shape. Apple engineers have defined a default var body: some View for any Shape:
When you declare your own var body: some View in a type that conforms to Shape, you are overriding the default var body that Apple engineers have defined, with your insufficient var body that doesnt contain anything much.
I haven't digged into it but there is a chance that the _ShapeView<Self, ForegroundStyle> that you can see in the picture does do some actual wizardry by accessing stuff that are internal and we don't have access to yet.
EDIT:
All that being said, i digged into this more as i was curious, and here's a working example:
import SwiftUI
struct ContentView: View {
var body: some View {
CustomShape()
CustomSHAPE()
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
print("CustomShape:", rect)
return Path { path in }
}
}
protocol SHAPE: Animatable, View {
func path(in rect: CGRect) -> Path
}
extension SHAPE {
var body: some View {
GeometryReader { geo in
self.path(in: geo.frame(in: .local))
}
}
}
struct CustomSHAPE: SHAPE {
func path(in rect: CGRect) -> Path {
print("CustomSHAPE:", rect)
return Path { path in }
}
}
in console you'll see something like this printed:
CustomShape: (0.0, 0.0, 390.0, 377.66666666666663)
CustomSHAPE: (0.0, 0.0, 390.0, 377.5)

How customise Slider blue line in SwiftUI?

Like in UISlider
let slider = UISlider()
slider.minimumTrackTintColor = .red
As pointed out in other answers you have limited ability to customize a Slider in SwiftUI. You can change the .accentColor(.red) but that only changes the minimumTrackTintColor.
Example of a Slider with .accentColor(.red)
Additionally, you can't change other things like thumbTintColor.
If you want more customization than just minimumTrackTintColor that you have no choice but to use a UISlider in SwiftUI as rob mayoff stated.
Here is some code on how you can use a UISlider in SwiftUI
struct SwiftUISlider: UIViewRepresentable {
final class Coordinator: NSObject {
// The class property value is a binding: It’s a reference to the SwiftUISlider
// value, which receives a reference to a #State variable value in ContentView.
var value: Binding<Double>
// Create the binding when you initialize the Coordinator
init(value: Binding<Double>) {
self.value = value
}
// Create a valueChanged(_:) action
#objc func valueChanged(_ sender: UISlider) {
self.value.wrappedValue = Double(sender.value)
}
}
var thumbColor: UIColor = .white
var minTrackColor: UIColor?
var maxTrackColor: UIColor?
#Binding var value: Double
func makeUIView(context: Context) -> UISlider {
let slider = UISlider(frame: .zero)
slider.thumbTintColor = thumbColor
slider.minimumTrackTintColor = minTrackColor
slider.maximumTrackTintColor = maxTrackColor
slider.value = Float(value)
slider.addTarget(
context.coordinator,
action: #selector(Coordinator.valueChanged(_:)),
for: .valueChanged
)
return slider
}
func updateUIView(_ uiView: UISlider, context: Context) {
// Coordinating data between UIView and SwiftUI view
uiView.value = Float(self.value)
}
func makeCoordinator() -> SwiftUISlider.Coordinator {
Coordinator(value: $value)
}
}
#if DEBUG
struct SwiftUISlider_Previews: PreviewProvider {
static var previews: some View {
SwiftUISlider(
thumbColor: .white,
minTrackColor: .blue,
maxTrackColor: .green,
value: .constant(0.5)
)
}
}
#endif
Then you can use this slider in your ContentView like this:
struct ContentView: View {
#State var sliderValue: Double = 0.5
var body: some View {
VStack {
Text("SliderValue: \(sliderValue)")
// Slider(value: $sliderValue).accentColor(.red).padding(.horizontal)
SwiftUISlider(
thumbColor: .green,
minTrackColor: .red,
maxTrackColor: .blue,
value: $sliderValue
).padding(.horizontal)
}
}
}
Example:
Link to full project
As of Apple's 2021 platforms, you can use the tint modifier to change the color of the track to the left of the slider knob. Beyond that, SwiftUI's Slider doesn't let you customize its appearance.
If you need more customization, then for now your only option is to create a UISlider and wrap it in a UIViewRepresentable. Work through the “Interfacing with UIKit” tutorial and watch WWDC 2019 Session 231: Integrating SwiftUI to learn how to use UIViewRepresentable.
The Slider documentation formerly mentioned a type named SliderStyle, but there is no documentation for SliderStyle and the type is not actually defined in the public interface of the SwiftUI framework as of Xcode 11 beta 4. It is possible that it will appear in a later release. It is also possible that we will have to wait for a future (after 13) version of SwiftUI for this ability.
If SliderStyle does appear, it might allow you to customize the appearance of a Slider in the same way that ButtonStyle lets you customize the appearance of Button—by assuming total responsibility for drawing it. So you might want to look for ButtonStyle tutorials on the net if you want to get a head start.
But SliderStyle might end up being more like TextFieldStyle. Apple provides a small number of TextFieldStyles for you to choose from, but you cannot define your own.
.accentColor(.red)
This will work on iOS and Mac Catalyst.
Check out customizable sliders example here
If the bright white slider handle grates on your dark mode design, you can use .label color and .softLight to tell it to simmer down. It looks good only in grayscale, unless you can figure out the blend modes and hue rotation.
The best looking result would be from an overlaid shaped with .blendMode(.sourceAtop)... but that blocks interaction, sadly.
#Environment(\.colorScheme) var colorScheme
var body: some View {
let hackySliderBGColor: Color = colorScheme == .dark ? Color(.secondarySystemBackground) : Color(.systemBackground)
let hackySliderAccentColor: Color = colorScheme == .dark ? Color(.label) : Color(.systemGray2)
let hackySliderBlendMode: BlendMode = colorScheme == .dark ? .softLight : .multiply
...
ZStack {
Rectangle()
.foregroundColor(hackySliderBGColor)
// This second Rect prevents a white sliver if slider is at max value.
.overlay(Rectangle()
.foregroundColor(hackySliderBGColor)
.offset(x: 5)
)
Slider(value: $pointsToScoreLimit,
in: themin...themax, step: 5)
.accentColor(hackySliderAccentColor)
.blendMode(hackySliderBlendMode)
}
Example:
So I tried to use .accentColor(.red) without success, so I noticed that for newer versions one has to use .tint(.red) to make the changes visible.
Hope this helps.
You can change the maximum track color using ZStack like this
var body: some View {
VStack {
Spacer()
Image("Fun").resizable().frame(width: 200, height: 200, alignment: .center).cornerRadius(20)
ZStack {
Rectangle()
.frame(height: 2)
.foregroundColor(.yellow).frame(width: UIScreen.main.bounds.width - 150)
Slider(value: $sliderval, in: 0...timeSlider_maximumValue, label: {Text("PLayer")}, minimumValueLabel: {Text("\(timeSlider_minimumValue)")}, maximumValueLabel: {Text("\(timeSlider_maximumValuetext)")}) { success in
SilderTap()
}.padding(.horizontal).tint(.green).foregroundColor(.white)
}
}
}
enter image description here