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)
Related
I have a pretty usual app with a TabView. However, when a particular process is happening in one of the content views, I would like to prevent the user from switching tabs until that process is complete.
If I use the disabled property on the TabView itself (using a #State binding to drive it), then the entire content view seems disabled - taps don't appear to be getting through to buttons on the main view.
Example:
struct FooView: View {
var body: some View {
TabView {
View1().tabItem(...)
View2().tabItem(...)
}
.disabled(someStateVal)
}
}
Obviously, I want the View1 to still allow the user to, you know, do things. When someStateVal is true, the entire View1 doesn't respond.
Is there a way to prevent changing tabs based on someStateVal?
Thanks!
I could not find a way to individually disable a tabItem, so here is
an example idea until someone comes up with more principled solution.
The trick is to cover the tab bar with a clear rectangle to capture the taps.
struct ContentView: View {
#State var isBusy = false
var body: some View {
ZStack {
TabView {
TestView(isBusy: $isBusy)
.tabItem {Image(systemName: "globe")}
Text("textview 2")
.tabItem {Image(systemName: "info.circle")}
Text("textview 3")
.tabItem {Image(systemName: "gearshape")}
}
VStack {
Spacer()
if isBusy {
Rectangle()
.fill(Color.white.opacity(0.001))
.frame(width: .infinity, height: 50)
}
}
}
}
}
struct TestView: View {
#Binding var isBusy: Bool
var body: some View {
VStack {
Text("TestView")
Button(action: {
isBusy.toggle()
}) {
Text("Busy \(String(isBusy))").frame(width: 170, height: 70)
}
}
}
}
I use another trick. Just hide the tab image.
struct FooView: View {
var body: some View {
TabView {
View1().tabItem{Image(systemName: someStateVal ? "": "globe")}
View2().tabItem{Image(systemName: someStateVal ? "": "gearshape")}
}
}
}
I want to implement a wizard whereby the user has to go through multiple screens in order to complete a signup process.
In SwiftUI the easiest way to do this is to have each view when it's finished push the next view on the navigation stack, but this codes the entire navigation between views in the views themselves, and I would like to avoid it.
What I want to do is have a parent view show the navigation view and then push the different steps on that navigation view.
I have something working already that looks like this:
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
NavigationView {
switch viewModel.state {
case .description:
AddDescriptionView(addDescriptionViewModel: AddVehicleDescriptionViewModel(), addVehicleViewModel: viewModel)
case .users:
AddUsersView(viewModel: AddUsersViewModel(viewModel.vehicle), addVehicleViewModel: viewModel)
}
}
}
}
This works fine. In the first step the AddVehicleViewModel is updated with the necessary info, the AddVehicleView is re-evaluated, the switch case jumps to the next option and the next view is presented to complete the wizard.
The issue with this however is that there are no navigation stack animations. Views simply get replaced. How can I change this to a system whereby the views are pushed, without implementing the push inside the AddDescriptionView object?
Should I write wrapper views that do the navigation stack handling on top of those views, and get rid of the switch case?
Ok so if you want to go from view a to b you should implement this not in your NavigationView but the view after the NavigationView, this way you wont break the animations. Why? Good question, I really don't know. When possible I keep my NavigationView always in the App struct under WindowGroup.
To get back to the point. Basically there should be an intermediate view between your steps and NavigationView. This view (StepperView) will contain the navigation logic of your steps. This way you keep the animations intact.
import SwiftUI
class AddVehicleViewModel: ObservableObject {
enum StateType {
case description
case users1
case users2
}
#Published var state: StateType? = nil
}
struct AddDescriptionView: View {
#ObservedObject var viewModel: AddVehicleViewModel
#State var text: String = ""
var body: some View {
GeometryReader {proxy in
VStack {
TextField("test", text: self.$text).background(RoundedRectangle(cornerRadius: 10).fill(Color.white).frame(width: 150, height: 40)).padding()
Button("1") {
viewModel.state = .users1
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center).background(Color.orange)
}
}
}
struct AddUsersView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("2") {
viewModel.state = .users2
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct AddUsersView2: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("3") {
viewModel.state = .description
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct StepperView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
VStack {
NavigationLink(
destination: AddDescriptionView(viewModel: viewModel),
isActive: .constant(viewModel.state == .description),
label: {EmptyView()})
if viewModel.state == .users1 {
NavigationLink(
destination: AddUsersView(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
if viewModel.state == .users2 {
NavigationLink(
destination: AddUsersView2(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
}.onAppear {
viewModel.state = .description
}
}
}
class BackBarButtonItem: UIBarButtonItem {
#available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
NavigationView {
NavigationLink(
destination: StepperView(),
isActive: .constant(true),
label: {EmptyView()})
}
}
}
So this started off as a single question but trying to answer the first question lead to me to the second. So the questions are:
My initial question was is it possible to animate a view by going from just false to true and not false to true. Say for example in the following code
Image(systemName: isTrue ? "heart.fill" : "heart")
.animation(.easeIn, value: isTrue)
The second question stemmed from the first because the parent view always sets the property to false (also the entire view re-renders due to other State properties) and while it re-renders it always animates the heart.
Is it not possible to change a property from the child view using #Binding? For some reason I cannot edit a value from the Binding (as seen below). I know #Binding does not own the view but I thought it had read / write capabilities.
Below is a gif showing what I mean and also the relevant code:
import SwiftUI
struct FirstView: View {
#State var isOn = false
var body: some View {
VStack {
ZStack {
RoundedRectangle(cornerRadius: 25.0)
.frame(height: 200)
Image(systemName: isOn ? "heart.fill" : "heart")
.foregroundColor(.red)
.scaleEffect(isOn ? 1.5 : 1.0)
.offset(x: 10.0, y: 10.0)
.animation(.easeIn)
}
Toggle(isOn: $isOn) {
Text("First View - Should change and animate")
}
}
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
struct SecondView: View {
#Binding var isOn: Bool
var body: some View {
VStack {
FirstView()
Toggle(isOn: $isOn, label: {
Text(" 2nd View - Change but don't animate")
})
}
}
}
struct SecondView_previews: PreviewProvider {
static var previews: some View {
SecondView(isOn: .constant(false))
}
}
Question 1
You need an animation that changes depending on isOn's state.
// on the view
.animation(animation, value: isOn)
// define the variable animation
var animation: Animation? {
isOn ? Animation.easeIn : nil
}
Question 2
You're already smart to use the value restriction version of .animation. That should restrict any changes in the hierarchy to just changes in that Binding value.
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())
}
}
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)