Conditionally apply overlay in SwiftUI - swiftui

I have an overlay button I would like to appear on the condition that we aren't on the first view!
On the first page, I would like the user to click this button to add users.
After that I would like users to navigate the form using this overlay
However, I cannot get the overlay to conditionally format so it does it if 'views > 1' and so it looks like this.
'''
//
// ContentView.swift
// PartyUp
//
// Created by Aarya Chandak on 3/9/22.
//
import SwiftUI
struct PartyPage: View {
#State private var viewModel = User.userList
#State var views = 0
#State private var cur = 0;
private var pages = 3;
var body: some View {
if(viewModel.isEmpty) {
VStack {
RSVPView()
}
} else {
ZStack {
VStack{
Text("Lets plan something!").padding()
Button(action: {views += 1}, label: { Image(systemName: "person.badge.plus")
})
}
if(views == 1) {
InviteScreen()
}
if(views == 2) {
PlanningScreen()
}
if(views == 3) {
ReviewScreen()
}
}
.overlay(
Button(action: {
withAnimation(.easeInOut) {
if(views <= totalPages){
views += 1;
}
else {
views = 0
}
}
}, label: {
Image(systemName: "chevron.right")
.font(.system(size:20, weight: .semibold))
.frame(width: 33, height: 33)
.background(.white)
.clipShape(Circle())
// Circuclar Slide
.overlay(
ZStack{
Circle()
.stroke(Color.black.opacity(0.04), lineWidth: 4)
.padding(-3)
Circle()
.trim(from: 0.0, to: CGFloat(views/pages))
.stroke(Color.white, lineWidth: 4)
.rotationEffect(.init(degrees: -90))
}
.padding(-3)
)
}
),alignment: .bottom).foregroundColor(.primary)
}
}
'''

Almost all modifiers accept a nil value for no change.
So basically you can write
.overlay(views > 1 ? Button(action: { ... }, label: { ... }) : nil)
It becomes more legible if you extract the button to an extra view struct.

Related

How to get rid of NavigationLink space

I have a problem with space occupied by NavigationLink. Following code:
struct EditView: View {
var body: some View {
NavigationView {
Form {
Section("Colors") {
ColorList(colors: viewModel.game.gameColors)
}
}
}
}
}
struct ColorList: View {
let colors: [String]
private let gridItemLayout = [GridItem(.adaptive(minimum: 44))]
var body: some View {
ScrollView {
LazyVGrid(columns: gridItemLayout) {
ForEach(colors, id: \.self) { colorName in
Meeple(colorName: colorName)
}
.padding(.vertical, 2)
}
}
}
}
// Meeple is just an image
struct Meeple: View {
// ...
var body: some View {
Image("meeple.2.fill")
.resizable()
.padding(5)
.foregroundColor(color.color)
.background(color.backgroundColor)
.frame(width: 44, height: 44, alignment: .center)
.clipShape(RoundedRectangle(cornerRadius: 5))
.shadow(color: .primary, radius: 5)
}
}
Produces a good result:
As soon as I add a NavigationLink around the ColorList like so
Section("Colors") {
NavigationLink(destination:
MultiColorPickerView(
selection: $viewModel.game.colors.withDefaultValue([])
)
) {
ColorList(colors: viewModel.game.gameColors)
}
}
The result looks weird:
There's plenty of space left. Why does it break after 3 items? And how can I make it to show more in one line?
add .frame(maxWidth: .infinity) to your ColorList.

Crash due index out of range -- although I'm sure index is not out of range

I have a parent view whose child view is any given index of an array. The index of the array is scrolled through by tapping buttons that increment or decrement the index which is stored in a State property.
However when the view is first initialized I get a crash, even though the State's initial value is always 0.
What is going on?
Code can be copied and pasted to reproduce error
import SwiftUI
struct ContentView: View {
#State private var shouldShowQuotes = false
var body: some View {
ZStack {
Color.orange
VStack {
Button(action: showQuotes){
Text("Get Quotes").bold()
.frame(maxWidth: 300)
}
// .controlProminence(.increased) //Safe to uncomment if Xcode 13
// .buttonStyle(.bordered)
// .controlSize(.large)
}
.fullScreenCover(isPresented: $shouldShowQuotes) {
QuoteScreen()
}
}.ignoresSafeArea()
}
private func showQuotes() {
self.shouldShowQuotes.toggle()
}
}
struct QuoteScreen: View {
#State private var quoteIndex = 0
var currentQuote: Quote {
return dummyData[quoteIndex]
}
var body: some View {
ZStack {
Color.orange
VStack {
QuoteView(quote: currentQuote)
Spacer()
HStack {
Button(action: degress) {
Image(systemName: "arrow.left.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
Button(action: progress) {
Image(systemName: "arrow.right.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
}
.padding(28)
//.buttonStyle(.plain) Safe to uncomment if Xcode 13
}
}.ignoresSafeArea()
}
private func progress() {
quoteIndex += 1
}
private func degress() {
quoteIndex -= 1
}
}
struct QuoteView: View {
#State private var showQuotes = false
let quote: Quote
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.stroke(lineWidth: 2)
VStack {
Text(quote.quote)
frame(maxWidth: 300)
Text(quote.author)
.frame(maxWidth: 300, alignment: .trailing)
.foregroundColor(.secondary)
}
}.navigationBarHidden(true)
.frame(height: 400)
.padding()
}
}
let dummyData = [Quote(quote: "The apple does not fall far from the tree", author: "Lincoln", index: 1),
Quote(quote: "Not everything that can be faced can be changed, but be sure that nothing can change until it is faced", author: "Unknown", index: 2),
Quote(quote: "Actions are but intentions", author: "Muhammad", index: 3)
]
struct Quote: Codable {
let quote: String
let author: String
let index: Int
}
When using arrays you always have to check that the element at the chosen index exist. This is how
I tested and modify your code to make it work.
(note: although this is just a test with dummyData, you need to decide if you want to scroll through the array index, or the Quote-index value, and adjust accordingly)
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
let dummyData = [
Quote(quote: "the index zero quote", author: "silly-billy", index: 0),
Quote(quote: "The apple does not fall far from the tree", author: "Lincoln", index: 1),
Quote(quote: "Not everything that can be faced can be changed, but be sure that nothing can change until it is faced", author: "Unknown", index: 2),
Quote(quote: "Actions are but intentions", author: "Muhammad", index: 3)
]
struct ContentView: View {
#State private var shouldShowQuotes = false
var body: some View {
ZStack {
Color.orange
VStack {
Button(action: showQuotes){
Text("Get Quotes").bold()
.frame(maxWidth: 300)
}
// .controlProminence(.increased) //Safe to uncomment if Xcode 13
// .buttonStyle(.bordered)
// .controlSize(.large)
}
.fullScreenCover(isPresented: $shouldShowQuotes) {
QuoteScreen()
}
}.ignoresSafeArea()
}
private func showQuotes() {
self.shouldShowQuotes.toggle()
}
}
struct QuoteScreen: View {
#State private var quoteIndex = 0
#State var currentQuote: Quote = dummyData[0] // <--- here, do not use "quoteIndex"
var body: some View {
ZStack {
Color.orange
VStack {
QuoteView(quote: $currentQuote) // <--- here
Spacer()
HStack {
Button(action: degress) {
Image(systemName: "arrow.left.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
Spacer()
Button(action: progress) {
Image(systemName: "arrow.right.square.fill")
.resizable()
.frame(width: 50, height: 50)
}
}
.padding(28)
//.buttonStyle(.plain) Safe to uncomment if Xcode 13
}
}.ignoresSafeArea()
}
// you will have to adjust this to your needs
private func progress() {
let prevValue = quoteIndex
quoteIndex += 1
if let thisQuote = dummyData.first(where: { $0.index == quoteIndex}) { // <--- here
currentQuote = thisQuote
} else {
quoteIndex = prevValue
}
}
// you will have to adjust this to your needs
private func degress() {
let prevValue = quoteIndex
quoteIndex -= 1
if let thisQuote = dummyData.first(where: { $0.index == quoteIndex}) { // <--- here
currentQuote = thisQuote
} else {
quoteIndex = prevValue
}
}
}
struct QuoteView: View {
#State private var showQuotes = false
#Binding var quote: Quote // <--- here
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.stroke(lineWidth: 2)
VStack {
Text(quote.quote)
.frame(maxWidth: 300) // <--- here missing leading "."
Text(quote.author)
.frame(maxWidth: 300, alignment: .trailing)
.foregroundColor(.secondary)
}
}.navigationBarHidden(true)
.frame(height: 400)
.padding()
}
}
struct Quote: Identifiable {
let id = UUID()
var quote: String
var author: String
var index: Int
}
This crash is not caused by the array access but by a typo in your code. You can see that if you run it in the simulator and look at the stack trace. It gets in an endless loop in the internals of SwiftUI. The reason is the missing dot before the frame modifier:
struct QuoteView: View {
#State private var showQuotes = false
let quote: Quote
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.stroke(lineWidth: 2)
VStack {
Text(quote.quote)
frame(maxWidth: 300) << !!!!! missing dot
Text(quote.author)
.frame(maxWidth: 300, alignment: .trailing)
.foregroundColor(.secondary)
}
}.navigationBarHidden(true)
.frame(height: 400)
.padding()
}
}
This calls the frame method on the QuoteView and not on the Text - which is an invalid operation.

SwiftUI Crash when calling scrollTo method when view is disappearing

I try to make my ScrollView fixed in a specific place, but the scrollTo method will cause the application to crash.
How to make the ScrollView stay in a fixed place?
I want to control the switching of views by MagnificationGesture to switch from one view to another.
But when Scroll() disappdars, the app crashs.
struct ContentView: View {
#State var tabCount:Int = 1
#State var current:CGFloat = 1
#State var final:CGFloat = 1
var body: some View {
let magni = MagnificationGesture()
.onChanged(){ value in
current = value
}
.onEnded { value in
if current > 2 {
self.tabCount += 1
}
final = current + final
current = 0
}
VStack {
VStack {
Button("ChangeView"){
self.tabCount += 1
}
if tabCount%2 == 0 {
Text("some text")
}else {
Scroll(current: $current)
}
}
Spacer()
HStack {
Color.blue
}
.frame(width: 600, height: 100, alignment: .bottomLeading)
}
.frame(width: 600, height: 400)
.gesture(magni)
}
}
This is ScrollView, I want it can appear and disappear. When MagnificationGesture is changing, scrollview can keep somewhere.
struct Scroll:View {
#Binding var current:CGFloat
let intRandom = Int.random(in: 1..<18)
var body: some View {
ScrollViewReader { proxy in
HStack {
Button("Foreword"){
proxy.scrollTo(9, anchor: .center)
}
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<20) { item in
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 100, height: 40)
.overlay(Text("\(item)").foregroundColor(.white))
.id(item)
}
}
.onChange(of: current, perform: { value in
proxy.scrollTo(13, anchor: .center)
})
}
}
}
}

Show hint view in LazyVGrid in SwiftUI

I have lots of button in a LazyVGrid in a ScrollView. I am trying to show a hint view just top of the button I clicked (as like keyboard popup). I don't know how do I catch the position of a ScrollView button. Besides need help to select suitable gesture to complete the task.
Graphical representation...
Here is my code:
struct ShowHint: View {
#State var isPressed: Bool = false
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 5)
var body: some View {
ZStack{
if isPressed {
ShowOnTopOfButton().zIndex(1)
}
ScrollView(showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 30) {
ForEach(0..<500) { i in
Text("\(i)")
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(Color.red.opacity( isPressed ? 0.5 : 0.9))
.gesture(TapGesture()
//.onStart { _ in isPressed = true } //but there is no property like this!
.onEnded { _ in isPressed = !isPressed }
)
}
}
}
.padding(.top, 50)
.padding(.horizontal, 10)
}
}
}
struct ShowOnTopOfButton: View {
var theS: String = "A"
var body: some View {
VStack {
Text("\(theS)")
.padding(20)
.background(Color.blue)
}
}
}
Here is possible solution - the idea is to show hint view as overlay of tapped element.
Tested with Xcode 12 / iOS 14
struct ShowHint: View {
#State var pressed: Int = -1
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 5)
var body: some View {
ZStack{
ScrollView(showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 30) {
ForEach(0..<500) { i in
Text("\(i)")
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(Color.red.opacity( pressed == i ? 0.5 : 0.9))
.gesture(TapGesture()
.onEnded { _ in pressed = pressed == i ? -1 : i }
)
.overlay(Group {
if pressed == i {
ShowOnTopOfButton()
.allowsHitTesting(false)
}}
)
}
}
}
.padding(.top, 50)
.padding(.horizontal, 10)
}
}
}

SwiftUI reduce spacing of rows in a list to null

I want to reduce the linespacing in a list to null.
My tries with reducing the padding did not work.
Setting ´.environment(.defaultMinListRowHeight, 0)´ helped a lot.
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self)
{ item in
Text("\(item)")
.padding(0)
//.frame(height: 60)
.background(Color.yellow)
}
//.frame(height: 60)
.padding(0)
.background(Color.blue)
}
.environment(\.defaultMinListRowHeight, 0)
.onAppear { UITableView.appearance().separatorStyle = .none }
.onDisappear { UITableView.appearance().separatorStyle = .singleLine }
}
}
}
Changing the ´separatorStyle´ to ´.none´ only removed the Line but left the space.
Is there an extra ´hidden´ view for the Lists row or for the Separator between the rows?
How can this be controlled?
Would be using ScrollView instead of a List a good solution?
ScrollView(.horizontal, showsIndicators: true)
{
//List {
ForEach(data, id: \.self)
{ item in
HStack{
Text("\(item)")
Spacer()
}
Does it also work for a large dataset?
Well, actually no surprise - .separatorStyle = .none works correctly. I suppose you confused text background with cell background - they are changed by different modifiers. Please find below tested & worked code (Xcode 11.2 / iOS 13.2)
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self)
{ item in
Text("\(item)")
.background(Color.yellow) // text background
.listRowBackground(Color.blue) // cell background
}
}
.onAppear { UITableView.appearance().separatorStyle = .none }
.onDisappear { UITableView.appearance().separatorStyle = .singleLine }
}
}
}
Update:
it's not possible to avoid the blue space between the yellow Texts?
Technically yes, it is possible, however for demo it is used hardcoded values and it is not difficult to fit some, while to calculate this dynamically might be challenging... anyway, here it is
it needs combination of stack for compression, content padding for resistance, and environment for limit:
List {
ForEach(data, id: \.self)
{ item in
HStack { // << A
Text("\(item)")
.padding(.vertical, 2) // << B
}
.listRowBackground(Color.blue)
.background(Color.yellow)
.frame(height: 12) // << C
}
}
.environment(\.defaultMinListRowHeight, 12) // << D
I do it the easy SwiftUI way:
struct ContentView: View {
init() {
UITableView.appearance().separatorStyle = .none
}
var body: some View {
List {
ForEach(0..<10){ item in
Color.green
}
.listRowInsets( EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) )
}
}
}
Reduce row spacing is really tricky, try
struct ContentView: View {
#State var data : [String] = ["first","second","3rd","4th","5th","6th"]
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)").font(.largeTitle)
.background(Color.yellow)
}.background(Color.green)
.padding(.leading, 10)
.padding(.bottom, -25)
.frame(maxWidth: .infinity)
}
}
}
}
}
It use ScrollView instead of List and negative padding.
I didn't find any solution based on List, we have to ask Apple to publish xxxxStyle protocols and underlying structures.
UPDATE
What about this negative padding value? For sure it depends on height of our row content and unfortunately on SwiftUI layout strategy. Lets try some more dynamic content! (we use zero padding to demostrate the problem to solve)
struct ContentView: View {
#State var data : [CGFloat] = [20, 30, 40, 25, 15]
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)").font(.system(size: item))
.background(Color.yellow)
}.background(Color.green)
.padding(.leading, 10)
//.padding(.bottom, -25)
.frame(maxWidth: .infinity)
}
}
}
}
}
Clearly the row spacing is not fixed value! We have to calculate it for every row separately.
Next code snippet demonstrate the basic idea. I used global dictionary (to store height and position of each row) and tried to avoid any high order functions and / or some advanced SwiftUI technic, so it is easy to see the strategy. The required paddings are calculated only once, in .onAppear closure
import SwiftUI
var _p:[Int:(CGFloat, CGFloat)] = [:]
struct ContentView: View {
#State var data : [CGFloat] = [20, 30, 40, 25, 15]
#State var space: [CGFloat] = []
func spc(item: CGFloat)->CGFloat {
if let d = data.firstIndex(of: item) {
return d < space.count ? space[d] : 0
} else {
return 0
}
}
var body: some View {
VStack {
ScrollView {
ForEach(data, id: \.self) { item in
VStack(alignment: .leading, spacing: 0) {
Color.red.frame(height: 1)
Text("\(item)")
.font(.system(size: item))
.background(Color.yellow)
}
.background(
GeometryReader { proxy->Color in
if let i = self.data.firstIndex(of: item) {
_p[i] = (proxy.size.height, proxy.frame(in: .global).minY)
}
return Color.green
}
)
.padding(.leading, 5)
.padding(.bottom, -self.spc(item: item))
.frame(maxWidth: .infinity)
}.onAppear {
var arr:[CGFloat] = []
_p.keys.sorted(by: <).forEach { (i) in
let diff = (_p[i + 1]?.1 ?? 0) - (_p[i]?.1 ?? 0) - (_p[i]?.0 ?? 0)
if diff < 0 {
arr.append(0)
} else {
arr.append(diff)
}
}
self.space = arr
}
}
}
}
}
Running the code I've got