Adding modifier to View's body - swiftui

Given I have a simple View like:
public struct MyView: View {
public var body: some View {
Text("My text 1")
Text("My text 1")
}
}
Is there an elegant way of setting a modifier on the View's body (such as setting AccessibilityIdentifier) straight in this struct without having to do this in the class that embeds MyView or creating another View layer?

A possible way is to wrap the content of the body in a Group
public struct MyView: View {
public var body: some View {
Group {
Text("My text 1")
Text("My text 1")
}
.font(.title)
}
}

Related

Getting onFocusChange callback for Buttons in SwiftUI (tvOS)

The onFocusChange closure in the focusable(_:onFocusChange:) modifier allows me to set properties for the parent view when child views are focused, like this:
struct ContentView: View {
#State var text: String
var body: some View {
VStack {
Text(text)
Text("top")
.padding()
.focusable(true, onFocusChange: { focused in
text = "top focus"
})
Text("bottom")
.padding()
.focusable(true, onFocusChange: { focused in
text = "bottom focus"
})
}
}
}
But in the 2020 WWDC video where focusable is introduced, it is clearly stated that this wrapper in not intended to be used with intrinsically focusable views such as Buttons and Lists. If I use Button in place of Text here the onFocusChange works, but the normal focus behaviour for the Buttons breaks:
struct ContentView: View {
#State var text: String
var body: some View {
VStack {
Text(text)
Button("top") {}
.padding()
.focusable(true, onFocusChange: { focused in
text = "top focus"
})
Button("bottom") {}
.padding()
.focusable(true, onFocusChange: { focused in
text = "bottom focus"
})
}
}
}
Is there any general way to get an onFocusChange closure to use with Buttons that doesn't break their normal focusable behaviour? Or is there some other way to accomplish this?
Try using #Environment(\.isFocused) and .onChange(of:perform:) in a ButtonStyle:
struct ContentView: View {
var body: some View {
Button("top") {
// button action
}
.buttonStyle(MyButtonStyle())
}
}
struct MyButtonStyle: ButtonStyle {
#Environment(\.isFocused) var focused: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: focused) { newValue in
// do whatever based on focus
}
}
}
IIRC using #Environment(\.isFocused) inside a ButtonStyle may only work on iOS 14.5+, but you could create a custom View instead of a ButtonStyle to support older versions.

SwiftUI ViewModifier for custom View

Is there a way to create a modifier to update a #State private var in the view being modified?
I have a custom view that returns either a Text with a "dynamic" background color OR a Circle with a "dynamic" foreground color.
struct ChildView: View {
var theText = ""
#State private var color = Color(.purple)
var body: some View {
HStack {
if theText.isEmpty { // If there's no theText, a Circle is created
Circle()
.foregroundColor(color)
.frame(width: 100, height: 100)
} else { // If theText is provided, a Text is created
Text(theText)
.padding()
.background(RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(color))
.foregroundColor(.white)
}
}
}
}
I re-use this view in different parts around my app. As you can see, the only parameter I need to specify is theText. So, the possible ways to create this ChildView are as follows:
struct SomeParentView: View {
var body: some View {
VStack(spacing: 20) {
ChildView() // <- Will create a circle
ChildView(theText: "Hello world!") // <- Will create a text with background
}
}
}
Nothing fancy so far. Now, what I need is to create (maybe) a modifier or the like so that in the parent views I can change the value of that #State private var color from .red to other color if I need more customization on that ChildView. Example of what I'm trying to achieve:
struct SomeOtherParentView: View {
var body: some View {
HStack(spacing: 20) {
ChildView()
ChildView(theText: "Hello world!")
.someModifierOrTheLike(color: Color.green) // <- what I think I need
}
}
}
I know I could just remove the private keyword from that var and pass the color as parameter in the constructor (ex: ChildView(theText: "Hello World", color: .green)), but I don't think that's the way to solve this problem, because if I need more customization on the child view I'd end up with a very large constructor.
So, Any ideas on how to achieve what I'm looking for? Hope I explained myself :)
Thanks!!!
It is your view and modifiers are just functions that generate another, modified, view, so... here is some possible simple way to achieve what you want.
Tested with Xcode 12 / iOS 14
struct ChildView: View {
var theText = ""
#State private var color = Color(.purple)
var body: some View {
HStack {
if theText.isEmpty { // If there's no theText, a Circle is created
Circle()
.foregroundColor(color)
.frame(width: 100, height: 100)
} else { // If theText is provided, a Text is created
Text(theText)
.padding()
.background(RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(color))
.foregroundColor(.white)
}
}
}
// simply modify self, as self is just a value
public func someModifierOrTheLike(color: Color) -> some View {
var view = self
view._color = State(initialValue: color)
return view.id(UUID())
}
}
Using a custom ViewModifier is indeed a way to help expose a simpler interface to users, but the general idea of how to pass customization parameters to a View (other than using an init), is via environment variables with .environment.
struct MyColorKey: EnvironmentKey {
static var defaultValue: Color = .black
}
extension EnvironmentValues {
var myColor: Color {
get { self[MyColorKey] }
set { self[MyColorKey] = newValue }
}
}
Then you could rely on this in your View:
struct ChildView: View {
#Environment(\.myColor) var color: Color
var body: some View {
Circle()
.foregroundColor(color)
.frame(width: 100, height: 100)
}
}
And the usage would be:
ChildView()
.environment(\.myColor, .blue)
You can make it somewhat nicer by using a view modifier:
struct MyColorModifier: ViewModifier {
var color: Color
func body(content: Content) -> some View {
content
.environment(\.myColor, color)
}
}
extension ChildView {
func myColor(_ color: Color) {
self.modifier(MyColorModifier(color: color)
}
}
ChildView()
.myColor(.blue)
Of course, if you have multiple customizations settings or if this is too low-level for the user, you could create a ViewModifier that exposes a subset of them, or create a type that encapsulates a style, like SwiftUI does with a .buttonStyle(_:)
Here's how you can chain methods based on Asperi`s answer:
struct ChildView: View {
#State private var foregroundColor = Color.red
#State private var backgroundColor = Color.blue
var body: some View {
Text("Hello World")
.foregroundColor(foregroundColor)
.background(backgroundColor)
}
func foreground(color: Color) -> ChildView {
var view = self
view._foregroundColor = State(initialValue: color)
return view
}
func background(color: Color) -> ChildView {
var view = self
view._backgroundColor = State(initialValue: color)
return view
}
}
struct ParentView: View {
var body: some View {
ChildView()
.foreground(color: .yellow)
.background(color: .green)
.id(UUID())
}
}

Avoid inherited styling inside List Section

When I add my custom widget into List's Section header, the elements inside my widget gets styled. Among others all the text become ALLCAPS. How can I avoid that styling, especially text capitalization?
struct MyHeader: View {
var body: some View {
Text("Hello wOrLd!")
}
}
struct ContentView: View {
var body: some View {
List {
Section(header: MyHeader()) {}
}
}
}
struct MyHeader: View {
var body: some View {
Text("Hello wOrLd!").textCase(.none)
}
}
Maybe it works

How to make a reusable TextPopUp in SwiftUI?

I am having trouble building a reusable SwiftUI text pop up view. I think the problem is the binding but I not sure. It is supposed to be a ContextMenu, but the reason I am not using ContextMenu is it does not show enough lines of text.
So far I have this...
struct TextPopUpView: View {
#EnvironmentObject var oracleViewModel: OracleViewModel
#Binding var showPopover: Bool
var displayedText: String
var popUpText: String
var body: some View {
Text("\(displayedText)")
.font(.title)
.fontWeight(.bold)
.onLongPressGesture {
self.showPopover = true
}
.popover(isPresented: $showPopover) {
Text("\(self.popUpText)")
.frame(width: 250.0)
.onTapGesture {
self.showPopover = false
}
}
}
}
And than I implement it for a particular view like so...
TextPopUpView(showPopover: $showPopover, displayedText: oracleViewModel.***someElement***, popUpText: oracleViewModel.getDescriptionFor***SomeElement***()).environmentObject(oracleViewModel)
where 'someElement' is the particular element text and popup text I want to show.
The problem is when I use TextPopUpView more than once in a view, the Popup view only displays the text for the last implementation on the said page.
I am guessing I am doing something wrong in the implementations of a reusable view, but I am not sure what. Any suggestions?
EDIT: (Example Used in code)
struct TopView: View {
#EnvironmentObject var oracleViewModel: OracleViewModel
#State private var showPopover: Bool = false
***Blarg Blarg Blarg***
VStack{
VStack {
Text("RUNE")
.font(.system(size:10))
.padding(.bottom, 5.0)
TextPopUpView(showPopover: $showPopover,
displayedText: oracleViewModel.rune,
popUpText: oracleViewModel.getDescriptionForRune()).environmentObject(oracleViewModel)
}
.padding(.bottom)
VStack {
Text("ELEMENT")
.font(.system(size:10))
.padding(.bottom, 5.0)
TextPopUpView(showPopover: $showPopover,
displayedText: oracleViewModel.element,
popUpText: oracleViewModel.getDescriptionForElement())
.environmentObject(oracleViewModel)
}
}
***Blarg Blarg Blarg***
}
(For some reason I only get the description for the Element, when I long press either view the TextPopUp is attached to)

Setting View visibility based on a property value?

When defining a view hierarchy using SwiftUI, is it possible to set the hidden() value of a View in the body of the definition?
For example:
var body: some View {
VStack(alignment: .leading) {
Text(self.name)
.font(.headline)
.hidden()
}
}
would hide the Text object, but I would like to use a boolean property to toggle visibility.
There is a way to do this using a ternary operator and the opacity value of the view, but I was hoping for a less clever solution.
If you don't want to use the opacity modifier this way:
struct ContentView: View {
#State private var showText = true
var body: some View {
VStack(alignment: .leading) {
Text("Hello world")
.font(.headline)
.opacity(showText ? 1 : 0)
}
}
}
you can decide to completely remove the view conditionally:
struct ContentView: View {
#State private var showText = true
var body: some View {
VStack(alignment: .leading) {
if showText {
Text("Hello world")
.font(.headline)
}
}
}
}
Consider that both ways are widely used in SwiftUI. For your specific case I'd honestly use the opacity modifier, but even the removal is fine.
Don't know if its still use useful because it's been a long time and I guess you found a solution since.But for anyone who's interested, we could create a modifier, which switches the visibility of the view according to a binding value :
import SwiftUI
struct IsVisibleModifier : ViewModifier{
var isVisible : Bool
// the transition will add a custom animation while displaying the
// view.
var transition : AnyTransition
func body(content: Content) -> some View {
ZStack{
if isVisible{
content
.transition(transition)
}
}
}
}
extension View {
func isVisible(
isVisible : Bool,
transition : AnyTransition = .scale
) -> some View{
modifier(
IsVisibleModifier(
isVisible: isVisible,
transition: transition
)
)
}
}
In use :
Text("Visible")
.isVisible(isVisible: isVisible)
.animation(.easeOut(duration: 0.3), value: isVisible)