I have a custom View that's basically wrapping Text with some additional functionality. E.g. formatting the text differently based on a value in the environment.
I want to use this custom view in place of a Text, i.e. I want to be able to use modifiers like .bold() on it.
Is it possible to return a Text from a View struct?
Here is 2 ways for you:
Way 1: You can use .bold() just on your CustomView!
struct CustomTextView: View {
#Environment(\.font) var environmentFont: Font?
private let boldAllowed: Bool
let string: String
private init(boldAllowed: Bool, string: String) {
self.boldAllowed = boldAllowed
self.string = string
}
init(_ string: String) {
self.init(boldAllowed: false, string: string)
}
var body: some View {
Text(string)
.font(boldAllowed ? environmentFont?.bold() : environmentFont)
.foregroundColor(Color.red) // <--- ::: some custom work here! :::
}
func bold() -> CustomTextView {
return CustomTextView(boldAllowed: true, string: string)
}
}
use case:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
CustomTextView("Hello, World!")
.bold()
CustomTextView("Hello, World!")
}
.font(Font.title.italic())
}
}
Result:
Way 2: You can use .bold() on any View you want!
I think this is the best possible way, because it is less code and usable on any view!
struct CustomBoldTextViewModifier: ViewModifier {
#Environment(\.font) var environmentFont: Font?
func body(content: Content) -> some View {
return content
.environment(\.font, environmentFont?.bold())
}
}
extension View {
func bold() -> some View {
return self.modifier(CustomBoldTextViewModifier())
}
}
use case:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
}
.bold()
.font(Font.title.italic())
}
}
Result:
You can manually get the view's body. Although this isn't recommended, it is possible. However a better solution may be to pass in a closure to modify your text, see 2nd solution. Both answers achieve the same thing.
Solution #1 (not recommended)
struct ContentView: View {
var body: some View {
CustomText("Custom text")
.getText()
.bold()
.environment(\.font, .largeTitle)
}
}
struct CustomText: View {
private let content: String
init(_ content: String) {
self.content = content
}
var body: some View {
// Apply whatever modifiers from environment, etc.
Text(content)
}
func getText() -> Text {
body as! Text
}
}
Here, the custom text is made bold.
Solution #2 (recommended)
This example you just pass in a closure of how to modify the already modified custom text. This is what I would recommend, plus it looks a lot cleaner.
struct ContentView: View {
var body: some View {
CustomText("Custom text") { text in
text
.bold()
.environment(\.font, .largeTitle)
}
}
}
struct CustomText<Content: View>: View {
private let content: String
private let transform: (Text) -> Content
init(_ content: String, transform: #escaping (Text) -> Content) {
self.content = content
self.transform = transform
}
var body: some View {
// Apply whatever modifiers from environment, etc.
let current = Text(content)
/* ... */
return transform(current)
}
}
Related
In my function, I have a parameter a manager: TextManager, but when I try to bind its text to a Binding<String>, it says "Cannot find '$manager' in scope". Why cannot it find the 'manager', it is right there in the function parameter?
The main UI code is the following:
struct BugView: View {
#ObservedObject var textManager = TextManager(limit: 30)
var body: some View {
VStack {
Text("line 1")
fetchTextView(title:"Name", manager: textManager)
Text("line 3")
}
}
#ViewBuilder
private func fetchTextView(title: String, manager : TextManager) -> some View {
TextField(title, text: $manager.text) // <- Cannot find '$manager' in scope
Text("\(manager.text.count)/\(manager.characterLimit)")
}
}
where the TextManager is the following
class TextManager: ObservableObject {
let characterLimit: Int
#Published var text = "" {
didSet {
if text.count > characterLimit && oldValue.count <= characterLimit {
text = oldValue
}
}
}
init(limit: Int = 10) {
characterLimit = limit
}
}
I started with this code, and it works fine
struct BugView: View {
#ObservedObject var textManager = TextManager(limit: 30)
var body: some View {
VStack {
Text("line 1")
TextField("good case", text: $textManager.text)
Text("line 3")
}
}
}
I just want to wrap the some text view generation logic inside a function because I would have multiple of those views, and I would like to avoid code duplication.
The first problem is TextManager is a class instead of struct. We only need reference types in SwiftUI if we are doing something asynchronous like saving or syncing data (although .task removes that need for most use cases).
struct TextManager {
let characterLimit: Int
var text = "" {
didSet {
if text.count > characterLimit && oldValue.count <= characterLimit {
text = oldValue
}
}
}
init(limit: Int = 10) {
characterLimit = limit
}
mutating func reset() {
text = ""
}
}
In a struct, any change to its properties, like text is detected as a change to the whole struct, which is a feature that SwiftUI takes advantage of in its design. For logic you might want to test, you can use mutating func.
Next, instead of a #ViewBuilder func you need a custom subview, e.g.
struct FetchTextView: View {
let title: String
#Binding var config: TextManager
var body: some View {
TextField(title, text: $config.text)
Text("\(config.text.count)/\(config.characterLimit)")
}
}
#Binding var instead of let gives us write access to the state, but as with let, body is also called when the value changes. The reason is breaking up the View struct hierarchy into small Views makes SwiftUI more efficient and faster, if you use funcs you risk breaking its dependency tracking (which is how SwiftUI decides if body should be called based on any used values changing). Use it like:
#State var config = TextManager()
FetchTextView(title: title, config: $config)
To use ObservableObject in function, you will have to create manual binding instead of the syntactic sugar $. The following code would work
#ViewBuilder
private func fetchTextView(title: String, manager: TextManager) -> some View {
let binding = Binding(
get: { manager.text },
set: { manager.text = $0 }
)
TextField(title, text: binding)
Text("\(manager.text.count)/\(manager.characterLimit)")
}
why can't we use it like this
just pass textManager directly instead pass via fetchTextView Method
struct BugView: View {
#StateObject var textManager = TextManager(limit: 30) // <-- when creating an instance make sure use StateObject
var body: some View {
VStack {
Text("line 1")
fetchTextView(title:"Name")
Text("line 3")
}
}
#ViewBuilder
private func fetchTextView(title: String) -> some View {
TextField(title, text: $textManager.text) // <-- just use textManagerDirectly...
Text("\(textManager.text.count)/\(textManager.characterLimit)")
}
}
I was wondering how to provide an empty state view in a list when the data source of the list is empty. Below is an example, where I have to wrap it in an if/else statement. Is there a better alternative for this, or is there a way to create a modifier on a List that'll make this possible i.e. List.emptyView(Text("No data available...")).
import SwiftUI
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
} else {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
}
}
struct EmptyListExample_Previews: PreviewProvider {
static var previews: some View {
EmptyListExample(objects: [])
}
}
I quite like to use an overlay attached to the List for this because it's quite a simple, flexible modifier:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.overlay(Group {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
}
})
}
}
}
It has the advantage of being nicely centred & if you use larger placeholders with an image, etc. they will fill the same area as the list.
One of the solutions is to use a #ViewBuilder:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
listView
}
#ViewBuilder
var listView: some View {
if objects.isEmpty {
emptyListView
} else {
objectsListView
}
}
var emptyListView: some View {
Text("Oops, loos like there's no data...")
}
var objectsListView: some View {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
You can create a custom modifier that substitutes a placeholder view when your list is empty. Use it like this:
List(items) { item in
Text(item.name)
}
.emptyPlaceholder(items) {
Image(systemName: "nosign")
}
This is the modifier:
struct EmptyPlaceholderModifier<Items: Collection>: ViewModifier {
let items: Items
let placeholder: AnyView
#ViewBuilder func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
extension View {
func emptyPlaceholder<Items: Collection, PlaceholderView: View>(_ items: Items, _ placeholder: #escaping () -> PlaceholderView) -> some View {
modifier(EmptyPlaceholderModifier(items: items, placeholder: AnyView(placeholder())))
}
}
I tried #pawello2222's approach, but the view didn't get rerendered if the passed objects' content change from empty(0) to not empty(>0), or vice versa, but it worked if the objects' content was always not empty.
Below is my approach to work all the time:
struct SampleList: View {
var objects: [IdentifiableObject]
var body: some View {
ZStack {
Empty() // Show when empty
List {
ForEach(objects) { object in
// Do something about object
}
}
.opacity(objects.isEmpty ? 0.0 : 1.0)
}
}
}
You can make ViewModifier like this for showing the empty view. Also, use View extension for easy use.
Here is the demo code,
//MARK: View Modifier
struct EmptyDataView: ViewModifier {
let condition: Bool
let message: String
func body(content: Content) -> some View {
valideView(content: content)
}
#ViewBuilder
private func valideView(content: Content) -> some View {
if condition {
VStack{
Spacer()
Text(message)
.font(.title)
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
Spacer()
}
} else {
content
}
}
}
//MARK: View Extension
extension View {
func onEmpty(for condition: Bool, with message: String) -> some View {
self.modifier(EmptyDataView(condition: condition, message: message))
}
}
Example (How to use)
struct EmptyListExample: View {
#State var objects: [Int] = []
var body: some View {
NavigationView {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.onEmpty(for: objects.isEmpty, with: "Oops, loos like there's no data...") //<--- Here
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("Add") {
objects = [1,2,3,4,5,6,7,8,9,10]
}
Button("Empty") {
objects = []
}
}
}
}
}
}
In 2021 Apple did not provide a List placeholder out of the box.
In my opinion, one of the best way to make a placeholder, it's creating a custom ViewModifier.
struct EmptyDataModifier<Placeholder: View>: ViewModifier {
let items: [Any]
let placeholder: Placeholder
#ViewBuilder
func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.modifier(EmptyDataModifier(
items: countries,
placeholder: Text("No Countries").font(.title)) // Placeholder. Can set Any SwiftUI View
)
}
}
Also via extension can little bit improve the solution:
extension List {
func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
modifier(EmptyDataModifier(items: items, placeholder: placeholder))
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.emptyListPlaceholder(
countries,
AnyView(ListPlaceholderView()) // Placeholder
)
}
}
If you are interested in other ways you can read the article
I have a simple View called TextView, the type of TextView is naturally View, I want make a modification on TextView like .foregroundColor(Color.red) and replace it again as itself! from my understanding my TextView is type View and should just working because the type is not Text But also I understand the complaining from SwiftUI which says cannot assign value of some View to type View. I like make some correction for solving issue, also I am not interested to add another initializer as foregroundColor.
import SwiftUI
struct ContentView: View {
#State private var myTextView: TextView?
var body: some View {
if let unwrappedMyTextView = myTextView {
unwrappedMyTextView
}
Button ("update") {
myTextView = TextView(stringOfText: "Hello, world!")//.foregroundColor(Color.red)
}
.padding()
}
}
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
}
var body: some View {
return Text(stringOfText)
}
}
The problem is that foregroundColor doesn't return TextView:
#inlinable public func foregroundColor(_ color: Color?) -> some View
Which means that
myTextView = TextView(stringOfText: "Hello, world!").foregroundColor(Color.red)
is no longer a TextView but some other View which is a result of applying foregroundColor.
Also, you shouldn't hold View properties as #State.
Try this instead:
struct ContentView: View {
var body: some View {
myTextView
}
var myTextView: some View {
TextView(stringOfText: "Hello, world!")
.foregroundColor(Color.red)
}
}
I have no clue. This is what I wrote. I know it's a futile attempt.
struct HelloWorld: View {
var body: some View {
Text("Hello, World!")
}
}
extension Text {
self.accessibility(hidden: true)
}
but I get this error
UPDATE:
Yes, I guess I can add a function in the extension or implement a struct. But I just want the Text View to inherit the accessibility modifier automatically.
so I can use it like this:
var body: some View {
Text("Hello, World!")
}
This is a struct implementation by #Asperi
//thanks for your answer Asperi, but
let sayHi = "hi"
var body: some View {
List {
Section(header: HidingAccessibility {Text(self.sayHi)}) {
Text("Hello, World!")
}
}
}
}
struct HidingAccessibility<Content: View>: View {
private var content: () -> Content
init(#ViewBuilder _ content: #escaping () -> Content ) {
self.content = content
}
var body: some View {
content().accessibility(hidden: true)
}
}
Here it is
struct HelloWorld: View {
var body: some View {
Text("Hello, World!")
.hideAccessibility() // << here !
}
}
extension Text {
public func hideAccessibility() -> some View { // << here !
self.accessibility(hidden: true)
}
}
or even in more generic way
struct HidingAccessibility<Content: View>: View {
private var content: () -> Content
init(#ViewBuilder _ content: #escaping () -> Content ) {
self.content = content
}
var body: some View {
content().accessibility(hidden: true)
}
}
struct HelloWorld: View {
var body: some View {
HidingAccessibility {
Text("Hello, World!") // << usual SwiftUI view building here
}
}
}
I am trying to add a ClearButton to TextField in SwiftUI when the particular TextField is selected.
The closest I got was creating a ClearButton ViewModifier and adding it to the TextField using .modifer()
The only problem is ClearButton is permanent and does not disappear when TextField is deselected
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier {
#Binding var text: String
public func body(content: Content) -> some View {
HStack {
content
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
}
}
}
}
Use ZStack to position the clear button appear inside the TextField.
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier
{
#Binding var text: String
public func body(content: Content) -> some View
{
ZStack(alignment: .trailing)
{
content
if !text.isEmpty
{
Button(action:
{
self.text = ""
})
{
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
.padding(.trailing, 8)
}
}
}
}
Use .appearance() to activate the button
var body: some View {
UITextField.appearance().clearButtonMode = .whileEditing
return TextField(...)
}
For reuse try with this:
func TextFieldUIKit(text: Binding<String>) -> some View{
UITextField.appearance().clearButtonMode = .whileEditing
return TextField("Nombre", text: text)
}
=== solution 1(best): Introspect https://github.com/siteline/SwiftUI-Introspect
import Introspect
TextField("", text: $text)
.introspectTextField(customize: {
$0.clearButtonMode = .whileEditing
})
=== solution 2: ViewModifier
public struct ClearButton: ViewModifier {
#Binding var text: String
public init(text: Binding<String>) {
self._text = text
}
public func body(content: Content) -> some View {
HStack {
content
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
.opacity(text == "" ? 0 : 1)
.onTapGesture { self.text = "" } // onTapGesture or plainStyle button
}
}
}
Usage:
#State private var name: String
...
Form {
Section() {
TextField("NAME", text: $name).modifier(ClearButton(text: $name))
}
}
=== solution 3: global appearance
UITextField.appearance().clearButtonMode = .whileEditing
You can add another Binding in your modifier:
#Binding var visible: Bool
then bind it to opacity of the button:
.opacity(visible ? 1 : 0)
then add another State for checking textField:
#State var showClearButton = true
And lastly update the textfield:
TextField("Some Text", text: $someBinding, onEditingChanged: { editing in
self.showClearButton = editing
}, onCommit: {
self.showClearButton = false
})
.modifier( ClearButton(text: $someBinding, visible: $showClearButton))
Not exactly what you're looking for, but this will let you show/hide the button based on the text contents:
HStack {
if !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle")
}
}
}
After initializing a new project we need to create a simple view modifier which we will apply later to our text field. The view modifier has the tasks to check for content in the text field element and display a clear button inside of it, if content is available. It also handles taps on the button and clears the content.
Let’s have a look at that view modifier:
import SwiftUI
struct TextFieldClearButton: ViewModifier {
#Binding var text: String
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = "" },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
)
}
}
}
}
The code itself should be self explanatory and easy to understand as there is no fancy logic included in our tasks.
We just wrap the textfield inside a HStack and add the button, if the text field is not empty. The button itself has a single action of deleting the value of the text field.
For the clear icon we use the delete.left icon from the SF Symbols 2 library by Apple, but you could also use another one or even your own custom one.
The binding of the modifier is the same as the one we apply to the text field. Without it we would not be able to check for content or clear the field itself.
Inside the ContentView.swift we now simply add a TextField element and apply our modifier to it — that’s all!
import SwiftUI
struct ContentView: View {
#State var exampleText: String = ""
var body: some View {
NavigationView {
Form {
Section {
TextField("Type in your Text here...", text: $exampleText)
.modifier(TextFieldClearButton(text: $exampleText))
.multilineTextAlignment(.leading)
}
}
.navigationTitle("Clear button example")
}
}
}
The navigation view and form inside of the ContentView are not required. You could also just add the TextField inside the body, but with a form it’s much clearer and beautiful. 🙈
And so our final result looks like this:
I found this answer from #NigelGee on "Hacking with Swift".
.onAppear {
UITextField.appearance().clearButtonMode = .whileEditing
}
It really helped me out.
Simplest solution I came up with
//
// ClearableTextField.swift
//
// Created by Fred on 21.11.22.
//
import SwiftUI
struct ClearableTextField: View {
var title: String
#Binding var text: String
init(_ title: String, text: Binding<String>) {
self.title = title
_text = text
}
var body: some View {
ZStack(alignment: .trailing) {
TextField(title, text: $text)
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
.onTapGesture {
text = ""
}
}
}
}
struct ClearableTextField_Previews: PreviewProvider {
#State static var text = "some value"
static var previews: some View {
Form {
// replace TextField("Original", text: $text) with
ClearableTextField("Clear me", text: $text)
}
}
}