Custom Scrollview in SwiftUI - if-statement

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

Related

Display a View when rest of content is empty

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)

On tap gesture on an image SwiftUI

So I am trying to change between views when someone clicks on the image, but the action after the ontapgesture is always "expression is unused". I tried changing it to a navigation view as well as other things but I feel like nothing seems to be working.
Code below:
struct MenuView2: View {
#State private var menuu2 = menu2()
var body: some View {
ScrollView(){
VStack {
ZStack {
Rectangle().frame(height:40).opacity(0.25).blur(radius: 10).onTapGesture {
print("breakfast tapped ")
}
HStack {
VStack(alignment: .leading, spacing: 8, content: {
Text("Breakfast").font(.largeTitle)
})
}
}
Image("breakfast").resizable().scaledToFill().onTapGesture {
menuu2
}
}
}
}
}
Thank you.
The error you get is "correct" in that menuu2 does not do anything, it is just there.
There are a number of ways to change view on tap, this is just one way:
struct MenuView2: View {
#State private var menuu2 = menuu2()
#State private var changeView = false
var body: some View {
changeView ? AnyView(theOtherView) : AnyView(theScrollView)
}
var theOtherView: some View {
// menuu2 presumably
// just for testing
Text("theOtherView").onTapGesture {
self.changeView = false
}
}
var theScrollView: some View {
ScrollView() {
VStack {
ZStack {
Rectangle().frame(height:40).opacity(0.25).blur(radius: 10).onTapGesture {
print("breakfast tapped ")
}
HStack {
VStack(alignment: .leading, spacing: 8, content: {
Text("Breakfast").font(.largeTitle)
})
}
}
Image("breakfast").resizable().scaledToFill().onTapGesture {
self.changeView = true
}
}
}
}
}

SwiftUI no hide animation

I have noticed that when I'm coloring the background I will not get animations when removing Views.
If I remove Color(.orange).edgesIgnoringSafeArea(.all) then hide animation will work, otherwise Modal will disappear abruptly. Any solutions?
struct ContentView: View {
#State var show = false
func toggle() {
withAnimation {
show = true
}
}
var body: some View {
ZStack {
Color(.orange).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Modal")
}
if show {
Modal(show: $show)
}
}
}
}
struct Modal: View {
#Binding var show: Bool
func toggle() {
withAnimation {
show = false
}
}
var body: some View {
ZStack {
Color(.systemGray4).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Close")
}
}
}
}
You need to make animatable container holding removed view (and this makes possible to keep animation in one place). Here is possible solution.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var show = false
func toggle() {
show = true // animation not requried
}
var body: some View {
ZStack {
Color(.orange).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Modal")
}
VStack { // << major changes
if show {
Modal(show: $show)
}
}.animation(.default) // << !!
}
}
}
struct Modal: View {
#Binding var show: Bool
func toggle() {
show = false // animation not requried
}
var body: some View {
ZStack {
Color(.systemGray4).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Close")
}
}
}
}

SwiftUI - Update variable in ForEach Loop to alternate background color of a list

I tried to implement a pingpong variable for a list so I could alternate the background color. For some reason the below throws and error but the compiler just says "Failed to Build." When I remove the "switchBit" function call from within the view, it compiles fine. Can someone help me understand what I am doing wrong here?
struct HomeScreen: View {
let colors: [Color] = [.green,.white]
#State var pingPong: Int = 0
var body: some View {
NavigationView{
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
ForEach(jobPostingData){jobposting in
NavigationLink(destination: jobPostingPage()) {
JobListingsRow(jobposting: jobposting).foregroundColor(Color.black).background(self.colors[self.pingPong])
}
self.switchBit()
}
}
.frame(width: geometry.size.width)
}
}
.navigationBarTitle(Text("Current Listed Positons"))
}
}
func switchBit() {
self.pingPong = (self.pingPong == 1) ? 0 : 1
}
}
I guess you want alternate coloraturas for the rows. You will have to avoid the switchBit code and use something like below to switch colours:
struct Homescreen: View {
let colors: [Color] = [.green,.white]
#State var jobPostingData: [String] = ["1","2", "3","4"]
#State var pingPong: Int = 0
var body: some View {
NavigationView{
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
ForEach(self.jobPostingData.indices, id: \.self) { index in
JobListingsRow(jobposting: self.jobPostingData[index])
.foregroundColor(Color.black)
.background(index % 2 == 0 ? Color.green : Color.red)
}
}
.frame(width: geometry.size.width)
}
}
.navigationBarTitle(Text("Current Listed Positons"))
}
}
}

SwiftUI ScrollView not getting updated?

Goal
Get data to display in a scrollView
Expected Result
Actual Result
Alternative
use List, but it is not flexible (can't remove separators, can't have multiple columns)
Code
struct Object: Identifiable {
var id: String
}
struct Test: View {
#State var array = [Object]()
var body: some View {
// return VStack { // uncomment this to see that it works perfectly fine
return ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
A not so hacky way to get around this problem is to enclose the ScrollView in an IF statement that checks if the array is empty
if !self.array.isEmpty{
ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
}
I've found that it works (Xcode 11.2) as expected if state initialised with some value, not empty array. In this case updating works correctly and initial state have no effect.
struct TestScrollViewOnAppear: View {
#State var array = [Object(id: "1")]
var body: some View {
ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
One hacky workaround I've found is to add an "invisible" Rectangle inside the scrollView, with the width set to a value greater than the width of the data in the scrollView
struct Object: Identifiable {
var id: String
}
struct ContentView: View {
#State var array = [Object]()
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
Rectangle()
.frame(width: geometry.size.width, height: 0.01)
ForEach(array) { o in
Text(o.id)
}
}
}
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
The expected behavior occurs on Xcode 11.1 but doesn't on Xcode 11.2.1
I added a frame modifier to the ScrollView which make the ScrollView appear
struct Object: Identifiable {
var id: String
}
struct ContentView: View {
#State var array = [Object]()
var body: some View {
ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
.frame(height: 40)
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
Borrowing from paulaysabellemedina, I came up with a little helper view that handles things the way I expected.
struct ScrollingCollection<Element: Identifiable, Content: View>: View {
let items: [Element]
let axis: Axis.Set
let content: (Element) -> Content
var body: some View {
Group {
if !self.items.isEmpty {
ScrollView(axis) {
if axis == .horizontal {
HStack {
ForEach(self.items) { item in
self.content(item)
}
}
}
else {
VStack {
ForEach(self.items) { item in
self.content(item)
}
}
}
}
}
else {
EmptyView()
}
}
}
}