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())
}
}
Related
Foreach on view must be presented with a View to process.
struct Home : View {
private var numberOfImages = 3
#State var isPresented : Bool = false
#State var currentImage : String = ""
var body: some View {
VStack {
TabView {
ForEach(1..<numberOfImages+1, id: \.self) { num in
Image("someimage")
.resizable()
.scaledToFill()
.onTapGesture() {
currentImage = "top_00\(num)"
isPresented.toggle()
}
}
}.fullScreenCover(isPresented: $isPresented, content: {FullScreenModalView(imageName: currentImage) } )
}
}
I'm trying to display an image in fullScreenCover. My problem is that the first image is empty. Yes, we can solve this defining at the beginning, however, this will complicate the code according to my experiences.
My question is, is it possible to assign a value to currentImage before the onTapGesture processed.
In short, what is the good practice here.
What you need is to use this modifier to present your full screen modal:
func fullScreenCover<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: #escaping (Item) -> Content) -> some View where Item : Identifiable, Content : View
You pass in a binding to an optional and uses the non optional value to construct a destination:
struct ContentView: View {
private let imageNames = ["globe.americas.fill", "globe.europe.africa.fill", "globe.asia.australia.fill"]
#State var selectedImage: String?
var body: some View {
VStack {
TabView {
ForEach(imageNames, id: \.self) { imageName in
Image(systemName: imageName)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageName
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageName in
Destination(imageName: imageName)
}
}
}
}
struct Destination: View {
let imageName: String
var body: some View {
ZStack {
Color.blue
Image(
systemName: imageName
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
}
}
You will have to make String identifiable for this example to work (not recommended):
extension String: Identifiable {
public var id: String { self }
}
Building upon #LuLuGaGa’s answer (accept that answer, not this one), instead of making String Identifiable, create a new Identifiable struct to hold the image info. This guarantees each image will have a unique identity, even if they use the same base image name. Also, the ForEach loop now becomes ForEach(imageInfos) since the array contains Identifiable objects.
Use map to turn image name strings into [ImageInfo] by calling the ImageInfo initializer with each name.
This example also puts the displayed image into its own view which can be dismissed with a tap.
import SwiftUI
struct ImageInfo: Identifiable {
let name: String
let id = UUID()
}
struct ContentView: View {
private let imageInfos = [
"globe.americas.fill",
"globe.europe.africa.fill",
"globe.asia.australia.fill"
].map(ImageInfo.init)
#State var selectedImage: ImageInfo?
var body: some View {
VStack {
TabView {
ForEach(imageInfos) { imageInfo in
Image(systemName: imageInfo.name)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageInfo
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageInfo in
ImageDisplay(info: imageInfo)
}
}
}
}
struct ImageDisplay: View {
let info: ImageInfo
#Environment(\.dismiss) var dismiss
var body: some View {
ZStack {
Color.blue
Image(
systemName: info.name
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
dismiss()
}
}
}
Given this code:
import SwiftUI
struct ContentView: View {
#State private var someView = DefaultView()
var body: some View {
VStack { someView }
//.onAppear { someView.change() } //1. Color not changing from here, isDisplay remains false
}
}
struct DefaultView: View {
#State private var isDisplay = false
func change() {
isDisplay = true
print("isDisplay = \(self.isDisplay)")
}
var body: some View {
Text("Hello!")
.foregroundColor(isDisplay ? .red : .blue)
//.onAppear { change() } //2. Color and isDisplay property is changing from here well
}
}
I'd like to call change func from parent view (ContentView 1.). That runs because the print is visible on the debug area, but there is no any changes, the isDisplay property does not change from false to true.
Why does this work that way? How can I change the #state property from the parent class?
Edit:
It could work:
struct DefaultView: View {
public var isDisplay: Bool
init() {
isDisplay = false
}
mutating func change() {
isDisplay = true
print("isDisplay = \(self.isDisplay)")
}
var body: some View {
Text("Hello!")
.foregroundColor(isDisplay ? .red : .blue)
}
}
The way you showed your code in question is not the way SwiftUI works, you are thinking in UIKit and coding SwiftUI?
Once you fired a View there is no return to use the imbedded function or any thing! SwiftUI-View is more like a gun build you can not change the direction or other property of it, after render is done! in the other hand UIKit-View is more like Rocket, after firing you have control on it and you can change destination and more other things as well. So there is deference between them, make your build lighter as possible. Because your going firing lots and SwiftUI should handel them as much as easily as possible.
Here the right way:
struct ContentView: View {
#State private var isDisplay: Bool = Bool()
var body: some View {
DefaultView(isDisplay: isDisplay)
Button("update") { isDisplay.toggle() }.padding()
}
}
struct DefaultView: View {
let isDisplay: Bool
var body: some View {
Text("Hello!")
.foregroundColor(isDisplay ? .red : .blue)
}
}
I would use #Binding to change the value of the property in your DefaultView from a parent view. Here is a brief example.
struct ContentView: View {
#State private var changeColor = false
var body: some View {
VStack {
Button {
changeColor.toggle()
} label: {
Text("Toggle Button")
.padding()
.foregroundColor(.red)
}
DefaultView(isDisplay: $changeColor)
}
}
}
struct DefaultView: View {
#Binding var isDisplay: Bool
var body: some View {
Text("Hello")
.foregroundColor(isDisplay ? .red : .blue)
}
}
I have a customContentViewFunction() that I like give an empty content like {} or EmptyView as default to it, which I do not have to do it in use case, for example I done same thing for color!
struct ContentView: View {
var body: some View {
customContentViewFunction(content: Text("hello"), color: .green)
// customContentViewFunction() : << Like this one!
}
func customContentViewFunction<Content: View>(content: Content, color: Color = Color.red) -> some View {
ZStack {
Rectangle()
.fill(color)
content
}
}
}
Define another version/signature of the function where it gets passed EmptyView() for content.
You could either choose to define it with the color property still there, or leave that out and let it get set to the default in the original signature.
struct ContentView: View {
var body: some View {
customContentViewFunction()
}
func customContentViewFunction(color: Color = Color.red) -> some View {
return customContentViewFunction(content: EmptyView(), color: color)
}
func customContentViewFunction<Content: View>(content: Content, color: Color = Color.red) -> some View {
ZStack {
Rectangle()
.fill(color)
content
}
}
}
With the new ScrollViewReader, it seems possible to set the scroll offset programmatically.
But I was wondering if it is also possible to get the current scroll position?
It seems like the ScrollViewProxy only comes with the scrollTo method, allowing us to set the offset.
Thanks!
It was possible to read it and before. Here is a solution based on view preferences.
struct DemoScrollViewOffsetView: View {
#State private var offset = CGFloat.zero
var body: some View {
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("Item \(i)").padding()
}
}.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") }
}.coordinateSpace(name: "scroll")
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
I found a version without using PreferenceKey. The idea is simple - by returning Color from GeometryReader, we can set scrollOffset directly inside background modifier.
struct DemoScrollViewOffsetView: View {
#State private var offset = CGFloat.zero
var body: some View {
ScrollView {
VStack {
ForEach(0..<100) { i in
Text("Item \(i)").padding()
}
}.background(GeometryReader { proxy -> Color in
DispatchQueue.main.async {
offset = -proxy.frame(in: .named("scroll")).origin.y
}
return Color.clear
})
}.coordinateSpace(name: "scroll")
}
}
I had a similar need but with List instead of ScrollView, and wanted to know wether items in the lists are visible or not (List preloads views not yet visible, so onAppear()/onDisappear() are not suitable).
After a bit of "beautification" I ended up with this usage:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
List(0..<100) { i in
Text("Item \(i)")
.onItemFrameChanged(listGeometry: geometry) { (frame: CGRect?) in
print("rect of item \(i): \(String(describing: frame)))")
}
}
.trackListFrame()
}
}
}
which is backed by this Swift package: https://github.com/Ceylo/ListItemTracking
The most popular answer (#Asperi's) has a limitation:
The scroll offset can be used in a function
.onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") }
which is convenient for triggering an event based on that offset.
But what if the content of the ScrollView depends on this offset (for example if it has to display it). So we need this function to update a #State.
The problem then is that each time this offset changes, the #State is updated and the body is re-evaluated. This causes a slow display.
We could instead wrap the content of the ScrollView directly in the GeometryReader so that this content can depend on its position directly (without using a State or even a PreferenceKey).
GeometryReader { geometry in
content(geometry.frame(in: .named(spaceName)).origin)
}
where content is (CGPoint) -> some View
We could take advantage of this to observe when the offset stops being updated, and reproduce the didEndDragging behavior of UIScrollView
GeometryReader { geometry in
content(geometry.frame(in: .named(spaceName)).origin)
.onChange(of: geometry.frame(in: .named(spaceName)).origin,
perform: offsetObserver.send)
.onReceive(offsetObserver.debounce(for: 0.2,
scheduler: DispatchQueue.main),
perform: didEndScrolling)
}
where offsetObserver = PassthroughSubject<CGPoint, Never>()
In the end, this gives :
struct _ScrollViewWithOffset<Content: View>: View {
private let axis: Axis.Set
private let content: (CGPoint) -> Content
private let didEndScrolling: (CGPoint) -> Void
private let offsetObserver = PassthroughSubject<CGPoint, Never>()
private let spaceName = "scrollView"
init(axis: Axis.Set = .vertical,
content: #escaping (CGPoint) -> Content,
didEndScrolling: #escaping (CGPoint) -> Void = { _ in }) {
self.axis = axis
self.content = content
self.didEndScrolling = didEndScrolling
}
var body: some View {
ScrollView(axis) {
GeometryReader { geometry in
content(geometry.frame(in: .named(spaceName)).origin)
.onChange(of: geometry.frame(in: .named(spaceName)).origin, perform: offsetObserver.send)
.onReceive(offsetObserver.debounce(for: 0.2, scheduler: DispatchQueue.main), perform: didEndScrolling)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.coordinateSpace(name: spaceName)
}
}
Note: the only problem I see is that the GeometryReader takes all the available width and height. This is not always desirable (especially for a horizontal ScrollView). One must then determine the size of the content to reflect it on the ScrollView.
struct ScrollViewWithOffset<Content: View>: View {
#State private var height: CGFloat?
#State private var width: CGFloat?
let axis: Axis.Set
let content: (CGPoint) -> Content
let didEndScrolling: (CGPoint) -> Void
var body: some View {
_ScrollViewWithOffset(axis: axis) { offset in
content(offset)
.fixedSize()
.overlay(GeometryReader { geo in
Color.clear
.onAppear {
height = geo.size.height
width = geo.size.width
}
})
} didEndScrolling: {
didEndScrolling($0)
}
.frame(width: axis == .vertical ? width : nil,
height: axis == .horizontal ? height : nil)
}
}
This will work in most cases (unless the content size changes, which I don't think is desirable). And finally you can use it like that :
struct ScrollViewWithOffsetForPreviews: View {
#State private var cpt = 0
let axis: Axis.Set
var body: some View {
NavigationView {
ScrollViewWithOffset(axis: axis) { offset in
VStack {
Color.pink
.frame(width: 100, height: 100)
Text(offset.x.description)
Text(offset.y.description)
Text(cpt.description)
}
} didEndScrolling: { _ in
cpt += 1
}
.background(Color.mint)
.navigationTitle(axis == .vertical ? "Vertical" : "Horizontal")
}
}
}
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)