How to add accessibility modifier to Text View using extension in SwiftUI - swiftui

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
}
}
}

Related

NSManaged Property causing DisclosureGroup animation problems in Swiftui

I have an NSManagedObject with NSManaged properties that control the expanded/collapsed state of disclosure groups. Here's an example:
/// Views that provides UI for app settings.
struct SettingsView: View {
#Binding var isExpanded: Bool
var body: some View {
let _ = Self._printChanges()
DisclosureGroup(isExpanded: $isExpanded, content: {
VStack {
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
}
}, label: {
HStack {
Text("Settings")
}
})
.padding([.leading,.trailing])
}
}
The view's parent calls it like this:
#EnvironmentObject var settings: SideBarSettings
.
.
.
SettingsView(isExpanded: $settings.isExpanded)
The disclosure group animation is lost when using NSManaged property. Animation is preserved when using any other non-NSManaged property even if the property is declared inside NSManagedObject.
Why is DisclosureGroup animation lost when using the NSManaged property?
After spending a few days on this, I'm accepting that the underlying problem is the way NSManaged properties work with SwiftUI. So, a possible solution would be to not use the NSManaged property at all in the DisclosureGroup and use a value type instead.
And use modifiers to init it and track changes on the new State var; like this:
struct SettingsView: View {
#Binding var isExpanded: Bool
#State private var isExpandedNonManaged = false // New property
var body: some View {
let _ = Self._printChanges()
DisclosureGroup(isExpanded: $isExpandedNonManaged, content: {
VStack {
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
Text("Hello world!")
}
}, label: {
HStack {
Text("Settings")
}
})
.onAppear {
isExpandedNonManaged = isExpanded // Initialize the State property
}
.onChange(of: isExpandedNonManaged) { newValue in
isExpanded = newValue // Update the managed property
}
}
}
Not elegant, nor scalable ... but it works.
Open to better solutions!
Update:
With a little help from this post, came up with a CustomDisclosureGroup that eliminates lots of code duplication.
public struct CustomDisclosureGroup<LabelContent: View, Content: View>: View {
var label: LabelContent
var content: Content
#Binding var isExpanded: Bool
public init(isExpanded: Binding<Bool>, #ViewBuilder label: () -> LabelContent, #ViewBuilder content: () -> Content) {
self.label = label()
self.content = content()
self._isExpanded = isExpanded
}
public init(labelString: String, isExpanded: Binding<Bool>, #ViewBuilder content: () -> Content) where LabelContent == Text {
self.init(isExpanded: isExpanded, label: { Text(labelString) }, content: content)
}
public var body: some View {
DisclosureGroup(isExpanded: $isExpanded) {
content
} label: {
label
}
}
}
#main
struct TestDisclosureApp: App {
#StateObject var topModel = TopModel()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#EnvironmentObject var topModel: TopModel
#State var isExpanded1 = false
#State var isExpanded2 = false
var body: some View {
CustomDisclosureGroup(isExpanded: $isExpanded1, label: { Text("Label") }) {
HStack {
Text("Content1")
}
}
.padding()
CustomDisclosureGroup(labelString: "Label", isExpanded: $isExpanded2) {
HStack {
Text("Content2")
}
}
.padding()
}
}

Return Text from a SwiftUI View

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)
}
}

Show a placeholder when a SwiftUI List is empty [duplicate]

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

How can I use a VStack as input for a func in SwiftUI?

I just made a func that take Text as input and add that Text to an array of AnyView, I want do same thing with a VStack, here my example code for Text which is working, just want same functionally for VStack, thanks for reading and your time.
struct ContentView: View {
init() {
appendThisText(text: Text("Hello, world1!") )
appendThisText(text: Text("Hello, world2!") )
appendThisText(text: Text("Hello, world3!") )
}
var body: some View {
ForEach(myAnyTexts.indices, id:\.self) { index in
myAnyTexts[index]
}
}
}
var myAnyTexts: [AnyView] = [AnyView]()
func appendThisText(text: Text) {
myAnyTexts.append(AnyView(text))
}
var myAnyVStacks: [AnyView] = [AnyView]()
func appendThisVStack(vStack: VStack<Content: View>) { // Here: the problem!
myAnyVStacks.append(AnyView(vStack))
}
updated for you to quality code:
var myAnyContents: [AnyView] = [AnyView]()
func appendThisContent<Content: View>(content: Content) {
myAnyContents.append(AnyView(content))
}
Add view constraint
func appendThisVStack<Content: View>(vStack: Content) { //<-- Here
myAnyVStacks.append(AnyView(vStack))
}

How to loop over viewbuilder content subviews in SwiftUI

So I’m trying to create a view that takes viewBuilder content, loops over the views of the content and add dividers between each view and the other
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
// here
}
.background(Color.black)
.cornerRadius(14)
}
}
so where I wrote “here” I want to loop over the views of the content, if that makes sense. I’ll write a code that doesn’t work but that explains what I’m trying to achieve:
ForEach(content.subviews) { view in
view
Divider()
}
How to do that?
I just answered on another similar question, link here. Any improvements to this will be made for the linked answer, so check there first.
GitHub link of this (but more advanced) in a Swift Package here
However, here is the answer with the same TupleView extension, but different view code.
Usage:
struct ContentView: View {
var body: some View {
BoxWithDividerView {
Text("Something 1")
Text("Something 2")
Text("Something 3")
Image(systemName: "circle") // Different view types work!
}
}
}
Your BoxWithDividerView:
struct BoxWithDividerView: View {
let content: [AnyView]
init<Views>(#ViewBuilder content: #escaping () -> TupleView<Views>) {
self.content = content().getViews
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(content.indices, id: \.self) { index in
if index != 0 {
Divider()
}
content[index]
}
}
// .background(Color.black)
.cornerRadius(14)
}
}
And finally the main thing, the TupleView extension:
extension TupleView {
var getViews: [AnyView] {
makeArray(from: value)
}
private struct GenericView {
let body: Any
var anyView: AnyView? {
AnyView(_fromValue: body)
}
}
private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
func convert(child: Mirror.Child) -> AnyView? {
withUnsafeBytes(of: child.value) { ptr -> AnyView? in
let binded = ptr.bindMemory(to: GenericView.self)
return binded.first?.anyView
}
}
let tupleMirror = Mirror(reflecting: tuple)
return tupleMirror.children.compactMap(convert)
}
}
Result:
So I ended up doing this
#_functionBuilder
struct UIViewFunctionBuilder {
static func buildBlock<V: View>(_ view: V) -> some View {
return view
}
static func buildBlock<A: View, B: View>(
_ viewA: A,
_ viewB: B
) -> some View {
return TupleView((viewA, Divider(), viewB))
}
}
Then I used my function builder like this
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#UIViewFunctionBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 0.0) {
content()
}
.background(Color(UIColor.AdUp.carbonGrey))
.cornerRadius(14)
}
}
But the problem is this only works for up to 2 expression views. I’m gonna post a separate question for how to be able to pass it an array