LazyGridView how to detect and act on item overflowing screen? - swiftui

I have a grid of items. Each item can expand height. I want to autoscroll when the item is expanded so it doesn't overflow the screen.
I was successful with the following code but I had to revert to a hack.
The idea was to detect when the item is overflowing using a Geometry reader on the item's background. Works wonders.
The issue is that when the view is expanded , the geo reader will update after the condition to check if autoscroll should execute is ran by the dispatcher. Hence my ugly hack.
Wonder what is the proper way ?
import SwiftUI
struct BlocksGridView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 300, maximum: .infinity), spacing: 20)]
var body: some View {
ZStack{
ScrollView {
ScrollViewReader { value in
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0..<20), id: \.self) {
BlockView(cardID: $0,scrollReader: value).id($0)
}
}
}
.padding(20)
}
}
}
}
struct BlockView : View {
var cardID : Int
var scrollReader : ScrollViewProxy
#State private var isOverflowingScreen = false
#State private var expand = false
var body: some View {
ZStack{
Rectangle()
.foregroundColor(isOverflowingScreen ? Color.blue : Color.green)
.frame(height: expand ? 300 : 135)
.clipShape(Rectangle()).cornerRadius(14)
.overlay(Text(cardID.description))
.background(GeometryReader { geo -> Color in
DispatchQueue.main.async {
if geo.frame(in: .global).maxY > UIScreen.main.bounds.maxY {
isOverflowingScreen = true
} else {
isOverflowingScreen = false
}
}
return Color.clear
})
.onTapGesture {
expand.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <-- Hack :(
if isOverflowingScreen {
withAnimation{
scrollReader.scrollTo(cardID)
}
}
}
}
}
}
}
struct BlocksGridView_Previews: PreviewProvider {
static var previews: some View {
BlocksGridView()
}
}
Blue items are overflowing ...

Related

SwiftUI: Animate View properties in response to Toggle change

I'm trying to change a view in response to changing a Toggle. I figure out a way to do it as long as I mirror the state value the toggle is using.
Is there a way to do it without creating another variable?
Here's my code. You can see that there are three animations triggered.
import SwiftUI
struct OverlayView: View {
#State var toggle: Bool = false
#State var animatedToggle: Bool = false
var body: some View {
VStack {
Image(systemName: "figure.arms.open")
.resizable()
.scaledToFit()
.foregroundColor(animatedToggle ? .green : .red) // << animated
.opacity(animatedToggle ? 1 : 0.2) // << animated
if animatedToggle { // << animated
Spacer()
}
Toggle("Overlay", isOn: $toggle)
.onChange(of: toggle) { newValue in
withAnimation {
animatedToggle = toggle
}
}
}
}
}
Just use the .animation view modifier
.animation(.default, value: toggle)
Then remove the second variable
struct OverlayView: View {
#State var toggle: Bool = false
var body: some View {
VStack {
Image(systemName: "figure.arms.open")
.resizable()
.scaledToFit()
.foregroundColor(toggle ? .green : .red)
.opacity(toggle ? 1 : 0.2)
if toggle {
Spacer()
}
Toggle("Overlay", isOn: $toggle)
}.animation(.default, value: toggle)
}
}

SwiftUI: How to pass an argument from one view to the next with dynamically generated buttons?

Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}

How to add/remove views from a paged ScrollView (TabView)

I am trying to create a paged scrollview
I have used a TabView to create this.
here is my code
struct TT: Identifiable {
let id = UUID()
var v: String
}
struct TTest: View {
#State var currentIndex = 0
#State var data = [
TT(v: "0")
]
var body: some View {
VStack {
SwiftUI.TabView(selection: $currentIndex) {
ForEach(data.indexed(), id: \.1.id) { index, value in
TTestConsomer(data: $data[index]).tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
Spacer()
HStack {
Image(systemName: "plus")
.resizable()
.frame(width: 50, height: 50)
.onTapGesture(perform: add)
Image(systemName: "minus")
.resizable()
.frame(width: 50, height: 5)
.onTapGesture(perform: delete)
}
Text("\(currentIndex + 1)/\(data.count)")
}
}
func add() {
data.append(TT(v: "\(data.count)") )
}
func delete() {
data.remove(at: currentIndex)
}
}
struct TTestConsomer: View {
#Binding var data: TT
var body: some View {
Text(data.v)
.padding(30)
.border(Color.black)
}
}
// You also need this extension
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
When I click on the + button, I can add more tabs.
But when I click on the delete button, My app crashes.
What is the problem here? There does not seem to be anything wrong with the code.
No, I don't think you're doing anything wrong. I submitted an issue to Apple about a related issue on the TabView a few months ago but never heard anything back. The debug comes from the collection view coordinator:
#0 0x00007fff57011ca4 in Coordinator.collectionView(_:cellForItemAt:) ()
It seems like after removing an item, the underlying collection view doesn't get updated. In UIKit, we would probably call .reloadData() to force it to update. In SwiftUI, you could redraw the collection view on the screen to force it to update. Obviously, this affects the UI and isn't a perfect solution.
struct TabViewTest: View {
#State var isLoading: Bool = false
#State var data: [String] = [
"one",
"two"
]
var body: some View {
if !isLoading, !data.isEmpty {
TabView {
ForEach(data, id: \.self) { item in
Text(item)
.tabItem { Text(item) }
}
}
.tabViewStyle(PageTabViewStyle())
.overlay(
HStack {
Text("Add")
.onTapGesture {
add()
}
Text("remove")
.onTapGesture {
remove()
}
}
, alignment: .bottom
)
}
}
func add() {
data.append("three")
print(data)
}
func remove() {
isLoading = true
data.removeFirst()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isLoading = false
}
print(data)
}
}
struct TabVIewTest_Previews: PreviewProvider {
static var previews: some View {
TabViewTest()
}
}

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 Transition / Animation

I am new to SwiftUI and have managed to build simple game.
All works except my plan to implement some sort of image animation / transition to introduce the image into the view.
The images are formatted correctly, but too abrupt and hoped to soften the image glow
I have tried by using the animation method, but has not mad a difference.
I hope someone can point me in the right direction.
var body: some View {
NavigationView {
ZStack {
Image("background")
.resizable()
.aspectRatio(contentMode: .fill)
.scaledToFill()
.edgesIgnoringSafeArea(.all)
VStack {
Text("Tap Correct Image")
.font(.headline)
.foregroundColor(Color.blue)
ForEach((0...2), id: \.self) { number in
Image(self.naijaObject[number])
.resizable()
.frame(width: 100, height: 100)
.border(Color.black,width: 1)
.transition(.slide)
.animation(.easeInOut(duration: 1))
.onTapGesture {
self.pictureTapped(number)
}
}
.navigationBarTitle(Text(processCorrectAnswer))
.alert(isPresented: $showAlert) {
Alert(title: Text("\(alertTitle), Your score is \(score)"), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}//End of VStack
}//End of
} //End of Naviagtion View
} //End of View
//Function to action which object is tapped
func pictureTapped(_ tag: Int) {
if tag == correctAnswer {
score += 1
alertTitle = "Correct"
} else {
score -= 1
alertTitle = "Wrong"
}
showAlert = true
}
//Function to continue the game
func askQuestion() {
naijaObject.shuffle()
correctAnswer = Int.random(in: 0...2)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I think I understand, what you want to achieve, but in future better to present more code. Here I used DispatchQueue.main.asyncAfter(deadline: .now() + 1) where I change the variable showAlert to true. And it works smoothly, hope it'll help you:
struct QuestionGame: View {
#State private var showAlert = false
#State private var score: Int = 0
#State private var correctAnswer = 1
#State private var tapped = false
var body: some View {
NavigationView {
VStack {
Text("Tap Correct Image")
.font(.headline)
ForEach((0...2), id: \.self) { number in
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(self.tapped && number == self.correctAnswer ? .green : .black) // you can do anything for animation
.animation(.easeInOut(duration: 1.0))
.onTapGesture {
self.pictureTapped(number)
}
}
.navigationBarTitle(Text("answer the question"))
.alert(isPresented: self.$showAlert) {
Alert(title: Text("Your score is \(score)"), dismissButton: .default(Text("Continue")) {
self.askQuestion()
})
}
}
}
}
func pictureTapped(_ tag: Int) {
tapped = true
if tag == correctAnswer {
score += 1
} else {
score -= 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.showAlert = true
}
}
//Function to continue the game
func askQuestion() {
tapped = false
showAlert = false
correctAnswer = Int.random(in: 0...2)
}
}
struct QuestionGame_Previews: PreviewProvider {
static var previews: some View {
QuestionGame()
}
}
P.S. and no, I didn't find the way to show alert smoothly. For this case may be better to use ZStack and change the alert (or any other view) opacity from 0 to 1 (with animation), when you tap on image.