I have a view body with logic such as this:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
This works fine to conditionally show elements vertically stacked. However, if none of the conditions are satisfied, the VStack is empty and my UI looks broken. I would like to show a placeholder instead. My current solution is to add a manual check at the end on !someCondition && !anotherCondition && !thirdCondition:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
if !someCondition && !anotherCondition && !thirdCondition { // 👈
Text("Please select an element.")
}
}
}
However, this is difficult to keep the condition in sync with the content above. I was hoping there was some sort of view modifier I could use such as:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}.emptyState { // 👈
Text("Please select an element.")
}
}
The closest thing I could find is this tutorial, but that requires passing in the condition as well.
Is there a way to build a view modifier like this emptyState which doesn't require duplicating the condition logic?
I was thinking I could use a ZStack for this:
var body: some View {
ZStack { // 👈
// empty state text
Text("Please select an element.")
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
}
... but then I run into a different issue where if I'm showing real content (e.g. SomeView()) but it's not large enough, I could see both SomeView() and the empty state text.
Here's one implementation using GeometryReader & it's named emptyState:
extension View {
func emptyState<Content: View>(#ViewBuilder content: () -> Content) -> some View {
return self.modifier(EmptyStateModifier(placeHolder: content()))
}
}
struct EmptyStateModifier<PlaceHolder: View>: ViewModifier {
#State var isEmpty = false
let placeHolder: PlaceHolder
func body(content: Content) -> some View {
ZStack {
if isEmpty {//Thanks to #Asperi
placeHolder
}
content
.background(
GeometryReader { reader in
Color.clear
.onChange(of: reader.frame(in: .global).size == .zero) { newValue in
isEmpty = reader.frame(in: .global).size == .zero
}
}
)
}
}
}
If it is a long chaining condition, you can handle it with switch{}, then use the benefit of default to display the placeholder when 0 condition is met(stack is empty or no selection)
#State var selected = ""
var body: some View {
VStack {
switch selected {
case "a":
SomeView()
case "b":
AnotherView()
case "c":
ThirdView()
//this default will show up
//when there is no selection
//and when the stack is empty meaning that all the above
//conditions did not meet
default:
Text("Please select an element")
}
}
}
There is no straightforward, officially supported way of telling what the return value of a view builder contains.
It is more sensible to handle this at the model layer than in your view. Each of your conditions are part of your model. These should be wrapped up into a single value type, and you can use the presence or absence of that (or an internal calculated value of that, depending on your requirements) to inform what to put in the stack. For example:
struct Model {
let one: Bool
let two: Bool
let three: Bool
}
struct MyView: View {
let model: Model?
var body: some View {
VStack {
switch model {
case .some(let model):
if model.one {
Text("One")
}
if model.two {
Text("Two")
}
if model.three {
Text("Three")
}
case .none:
Text("Empty")
}
}
}
}
(I'm assuming here that there is no valid Model which doesn't contain any of the values, that would be when it is set to nil)
Related
I have a container view that contains multiple child views. These child views have different transitions that should be applied when the container view is inserted or removed.
Currently, when I add or remove this container view, the only transition that works is the one applied directly to the container view.
I have tried applying the transitions to each child view, but it doesn't work as expected. Here is a simplified version of my code:
struct Container: View, Identifiable {
let id = UUID()
var body: some View {
HStack {
Text("First")
.transition(.move(edge: .leading)) // this transition is ignored
Text("Second")
.transition(.move(edge: .trailing)) // this transition is ignored
}
.transition(.opacity) // this transition is applied
}
}
struct Example: View {
#State var views: [AnyView] = []
func pushView(_ view: some View) {
withAnimation(.easeInOut(duration: 1)) {
views.append(AnyView(view))
}
}
func popView() {
guard views.count > 0 else { return }
withAnimation(.easeInOut(duration: 1)) {
_ = views.removeLast()
}
}
var body: some View {
VStack(spacing: 30) {
Button("Add") {
pushView(Container()) // any type of view can be pushed
}
VStack {
ForEach(views.indices, id: \.self) { index in
views[index]
}
}
Button("Remove") {
popView()
}
}
}
}
And here's a GIF that shows the default incorrect behaviour:
If I remove the container's HStack and make the children tuple views, then the individual transitions will work, but I will essentially lose the container — which in this scenario was keeping the children aligned next to each other.
e.g
So this isn't a useful solution.
Note: I want to emphasise that the removal transitions are equally important to me
The .transition is applied to the View that appears (or disappears), and as you've found any .transition on a subview is ignored.
You can work around this by adding your Container without animation, and then animating in each of the Text.
struct Pair: Identifiable {
let id = UUID()
let first = "first"
let second = "second"
}
struct Container: View {
#State private var showFirst = false
#State private var showSecond = false
let pair: Pair
var body: some View {
HStack {
if showFirst {
Text(pair.first)
.transition(.move(edge: .leading))
}
if showSecond {
Text(pair.second)
.transition(.move(edge: .trailing))
}
}
.onAppear {
withAnimation {
showFirst = true
showSecond = true
}
}
}
}
struct ContentView: View {
#State var pairs: [Pair] = []
var animation: Animation = .easeInOut(duration: 1)
var body: some View {
VStack(spacing: 30) {
Button("Add") {
pairs.append(Pair())
}
VStack {
ForEach(pairs) { pair in
Container(pair: pair)
}
}
Button("Remove") {
if pairs.isEmpty { return }
withAnimation(animation) {
_ = pairs.removeLast()
}
}
}
}
}
Also note, your ForEach should be over an array of objects rather than Views (not that it makes a difference in this case).
Update
You can reverse the process by using a Binding to a Bool that contains the show state for each View. In this case I've created a struct PairState that holds a Set of all the views currently shown:
struct Container: View {
let pair: Pair
#Binding var show: Bool
var body: some View {
HStack {
if show {
Text(pair.first)
.transition(.move(edge: .leading))
Text(pair.second)
.transition(.move(edge: .trailing))
}
}
.onAppear {
withAnimation {
show = true
}
}
}
}
struct PairState {
var shownIds: Set<Pair.ID> = []
subscript(pairID: Pair.ID) -> Bool {
get {
shownIds.contains(pairID)
}
set {
shownIds.insert(pairID)
}
}
mutating func remove(_ pair: Pair) {
shownIds.remove(pair.id)
}
}
struct ContentView: View {
#State var pairs: [Pair] = []
#State var pairState = PairState()
var body: some View {
VStack(spacing: 30) {
Button("Add") {
pairs.append(Pair())
}
VStack {
ForEach(pairs) { pair in
Container(pair: pair, show: $pairState[pair.id])
}
}
Button("Remove") {
guard let pair = pairs.last else { return }
Task {
withAnimation {
pairState.remove(pair)
}
try? await Task.sleep(for: .seconds(0.5)) // 😢
_ = pairs.removeLast()
}
}
}
}
}
This has a delay in there to wait for the animation to complete before removing from the array. I'm not happy with that, but it works in this example.
Is there anyway of checking if a view added using ViewBuilder is an EmptyView. Here is a code sample:
struct ViewAsVariable<TestView:View>: View {
#ViewBuilder var aView:TestView
var body: some View {
if aView === EmptyView(){
Text( "view is empty")
}
else{
aView
}
}
init(destinationView: TestView ) {
self.aView = destinationView
}
}
You should test what type the associated generic type is rather than the instance of that type
if TestView.self == EmptyView.self { ...
You don't need a type check because EmptyView isn't going to give you anything useful to work with; use Never instead.
struct ViewAsVariable<TestView: View>: View {
let aView: TestView?
init(destinationView: TestView = Never?.none) {
aView = destinationView
}
var body: some View {
if let aView {
aView
} else {
Text("view is empty")
}
}
}
Type checks at runtime are rather unswifty.
And unlike UIKit SwiftUI is declarative. You should tell the view that it's empty rather than asking for it
struct ViewAsVariable<TestView:View>: View {
private let isEmpty : Bool
#ViewBuilder var aView:TestView
var body: some View {
if isEmpty {
Text( "view is empty")
}
else {
aView
}
}
init(destinationView: TestView, isEmpty: Bool = false ) {
self.isEmpty = isEmpty
self.aView = destinationView
}
}
I have a piece of code that looks like this in my ContentView and I'd like to not repeat the ScrollView's modifiers unnecessarily.
if variable == false {
ScrollView {
// Code here
}
.navigationBarTitleDisplayMode(.inline)
.toolbar() {
// Code here
}
} else {
ScrollView(.horizontal, showsIndicators: false) {
// Code here
}
.navigationBarTitleDisplayMode(.inline)
.toolbar() {
// Code here
}
So I tried to do this instead:
ScrollView(variable ? .horizontal, showsIndicators: false : nil) {
// Code here
}
.navigationBarTitleDisplayMode(.inline)
.toolbar() {
// Code here
}
But it doesn't work. How do you guys do this kind of thing?
Maybe, I should create my own custom scrollview, but how ?
struct ScrollViewCustom /* What to write here? */ View {
#AppStorage("variable") private var variable = false
#ViewBuilder /* What to write here? */
var body: some View {
if variable == false {
ScrollView()
} else {
ScrollView(.horizontal, showsIndicators: false)
}
}
}
Thanks in advance!
The ternary operator ? always needs two options (for true/false). So you can do:
ScrollView(variable ? .horizontal : .vertical, showsIndicators: false)
But keep in mind that inside the ScrollView you'll then need to switch between a VStack or HStack depending on Scroll direction.
So actually your custom approach might be more useful. It goes like this:
struct ScrollViewCustom<V: View>: View {
#AppStorage("variable") private var variable = false
#ViewBuilder var content: () -> V
var body: some View {
if variable == false {
ScrollView(.vertical) {
VStack {
content()
}
}
} else {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
content()
}
}
}
}
}
and it would be used like this:
struct ContentView: View {
#AppStorage("variable") private var variable = false
var body: some View {
NavigationStack {
ScrollViewCustom {
ForEach(0..<6) { item in
Text("Item \(item)")
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar() {
Button("Switch") { variable.toggle() }
}
}
}
}
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
In SwiftUI I've created a struct that should create different overlay views depending on some state variables. If any of the state booleans is true, then it should return custom view (either ErrorOverlay or LoadingOverlay or else an EmptyView) like this:
struct OverlayContainer: View {
#State var isLoading: Bool = false
#State var isErrorShown: Bool = false
func setIsLoading(isLoading: Bool) {
self.isLoading = isLoading
}
func setIsErrorShown(isErrorShown: Bool) {
self.isErrorShown = isErrorShown
}
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
Now I've implemented the overlay on some content in the Home view with buttons that should change the state and show the correct overlay, like this:
struct Home: View {
var body: some View {
let overlayContainer = OverlayContainer()
return HStack {
// Some more content here
Button(action: {
overlayContainer.setIsLoading(isLoading: true)
}) {
Text("Start loading")
}
Button(action: {
overlayContainer.setIsErrorShown(isErrorShown: true)
}) {
Text("Show error")
}
}.overlay(overlayContainer)
}
}
This isn't working: when I click the button nothing happens. Why and how to solve this? (without using binding, see below)
ps. I've been able to get a working solution by doing the following:
extracting the state booleans to the Home view
pass these through the constructor of the OverlayContainer
change the state booleans instead of calling the set methods when clicking the buttons
change the OverlayContainer so it implements an init method with both booleans
change the state booleans in the OverlayContainer to bindings.
However, I'd like to implement the states in the OverlayContainer to be able to re-use that in different screens, without implementing state variables in all of these screens. Firstly because there will probably be more cases than just these 2. Secondly because not all screens will need to access all states and I haven't found out a simple way to implement optional bindings through the init method.
To me it feels that all these states belong to the OverlayContainer, and changing the state should be as short and clean as possible. Defining states everywhere feels like code duplication. Maybe I need a completely different architecture?
It should be used Binding instead. Here is possible solution.
struct OverlayContainer: View {
#Binding var isLoading: Bool
#Binding var isErrorShown: Bool
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
struct Home: View {
#State var isLoading: Bool = false
#State var isErrorShown: Bool = false
var body: some View {
HStack {
// Some more content here
Button(action: {
self.isLoading = true
}) {
Text("Start loading")
}
Button(action: {
self.isErrorShown = true
}) {
Text("Show error")
}
}.overlay(OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown))
}
}
To make it the way you want, use Binding:
struct OverlayContainer: View {
#Binding var isLoading: Bool
#Binding var isErrorShown: Bool
func setIsLoading(isLoading: Bool) {
self.isLoading = isLoading
self.isErrorShown = !isLoading
}
func setIsErrorShown(isErrorShown: Bool) {
self.isErrorShown = isErrorShown
self.isLoading = !isErrorShown
}
var body: some View {
Group {
if(isErrorShown) {
ErrorOverlay()
}
else if(isLoading) {
LoadingOverlay()
}
else {
EmptyView()
}
}
}
}
struct Home: View {
#State var isLoading = false
#State var isErrorShown = false
var body: some View {
let overlayContainer = OverlayContainer(isLoading: $isLoading, isErrorShown: $isErrorShown)
return HStack {
// Some more content here
Button(action: {
overlayContainer.setIsLoading(isLoading: true)
}) {
Text("Start loading")
}
Button(action: {
overlayContainer.setIsErrorShown(isErrorShown: true)
}) {
Text("Show error")
}
}.overlay(overlayContainer)
}
}