ScrollView stops components from expanding - swift

I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
Here is my code:
import SwiftUI
struct DisciplineView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
Card(cardTitle: "Notes")
Card(cardTitle: "Planner")
Card(cardTitle: "Homeworks / Exams")
}
.ignoresSafeArea()
}
}
}
struct DisciplineV_Previews: PreviewProvider {
static var previews: some View {
DisciplineView()
}
}
import SwiftUI
struct Card: View {
#State var cardTitle = ""
#State private var isTapped = false
var body: some View {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.foregroundColor(.gray.opacity(0.2))
.frame(width: .infinity, height: isTapped ? .infinity : 50)
.background(
VStack {
cardInfo
if(isTapped) { Spacer() }
}
.padding(isTapped ? 10 : 0)
)
}
var cardInfo: some View {
HStack {
Text(cardTitle)
.font(.title).bold()
.foregroundColor(isTapped ? .white : .black)
.padding(.leading, 10)
Spacer()
Image(systemName: isTapped ? "arrowtriangle.up.square.fill" : "arrowtriangle.down.square.fill")
.padding(.trailing, 10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
}
}
here is almost the same as I would like to have, but I would like the first one to be on the whole screen and stop the ScrollView while appearing.
Thank you!
Described above:
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)

I think this is pretty much what you are trying to achieve.
Basically, you have to scroll to the position of the recently presented view and disable the scroll. The scroll have to be disabled enough time to avoid continuing to the next item but at the same time, it have to be enabled soon enough to give the user the feeling that it is scrolling one item at once.
struct ContentView: View {
#State private var canScroll = true
#State private var itemInScreen = -1
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0...10, id: \.self) { item in
Text("\(item)")
.onAppear {
withAnimation {
proxy.scrollTo(item)
canScroll = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
canScroll = true
}
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Color.blue)
}
}
}
.disabled(!canScroll)
}
.ignoresSafeArea()
}
}

Related

How to hide a swiftUI view by touching anywhere outside of it

I have a view that call an alert that is another smaller view, whenever the second View is shown, I want to hide it when clicking outside of it.
How can I do that?
struct AlertView: View{
let screenSize = UIScreen.main.bounds
#Binding var alertIsShown: Bool
var body: some View{
VStack{
Button("Cancel") {
self.alertIsShown=false
}
}.padding()
.frame(width: screenSize.width * 0.85, height: screenSize.height * 0.6)
.background(Color(red: 0.4627, green: 0.8392, blue: 1.0))
.clipShape(RoundedRectangle(cornerRadius: 20.0, style: .continuous))
.offset(y: alertIsShown ? 0 : screenSize.height)
.animation(.spring())
.shadow(color: Color(.white), radius: 6, x: -9, y: -0)
}
}
}
For the main view that call the alert:
struct MainView: View {
#State private var alertIsShown = false
#State var liveOrdersList: [String] = ["item-1", "item-2"]
var body: some View {
VStack{
NavigationView{
List{
ForEach(liveOrdersList, id: \.self) { order in
HStack {
VStack(alignment: .leading){
Text("\(order.totalPrice)")
}
Spacer()
Button("add") {
withAnimation(.linear(duration: 0.3)) {
alertIsShown.toggle()
}
}
}
}
}
}
}
}
if alertIsShown{ //here I call the aler
AlertView(alertIsShown: $alertIsShown)
}
}
The list of buttons call the alert view.
How can I hide it when tapping outside of it?
You could try this approach, using .simultaneousGesture(...), as shown in this example code, to hide the AlertView by touching anywhere outside of it.
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#State var alertIsShown = false
// for testing
#State var liveOrdersList: [String] = ["item-1", "item-2", "item-3", "item-4", "item-5"]
var body: some View {
NavigationView {
VStack {
List {
ForEach(liveOrdersList, id: \.self) { order in
HStack {
VStack(alignment: .leading) {
Text("\(order)")
}
Spacer()
Button("add") {
withAnimation(.linear(duration: 0.3)) {
alertIsShown.toggle()
}
}
}
}
}
// -- here
.simultaneousGesture(alertIsShown ? TapGesture().onEnded {
alertIsShown = false
} : nil)
if alertIsShown {
AlertView(alertIsShown: $alertIsShown)
}
}
}
}
}

Trying to create a grid of numbers that must be clicked in order and when clicked the background changes color SWIFTUI

As the title says I am trying to create a grid of random numbers that must be clicked in ascending order and when pressed the background changes color so they're no longer visible.
I figured out how to create the grid from 0 to what ever size grid I want (5x5, 10x10, etc...)
I want the background to be white to start and then when the button is pressed it changes to black
The two biggest issues I'm having are all of the buttons turning black after I press on one button and the numbers randomize every time I press a button.
Any one have any ideas?
import SwiftUI
struct ContentView: View {
#State var buttonbackg = Color.white
#State var gridsize = 100
var body: some View {
//make grid
let cellCount = (gridsize/10)
let r = numbrand(min: 00, max: ((gridsize/10) * (gridsize/10) - 1))
ZStack{
Rectangle()
.ignoresSafeArea()
.background(Color.black)
VStack{
Text("Concentration Grid")
.font(.title)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom)
Spacer()
}
VStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { i in
HStack(spacing:3) {
Spacer()
ForEach(1...cellCount, id:\.self) { c in
let a = r()
ZStack {
Button(action:{
if (self.buttonbackg == .white) {
self.buttonbackg = .black
}
}){
Text("\(a)")
.foregroundColor(Color.black)
.frame(width: 28, height: 28)
.background(buttonbackg)
.cornerRadius(5)
.multilineTextAlignment(.center)
.scaledToFit()
.padding(.all, -2)
Spacer()
}
Spacer()
}
}
}
Spacer()
}
Spacer()
}
}
}
//^grid
func numbrand(min: Int, max:Int) -> () -> Int{
//let array: [Int] = Array(min...max)
var numbers: [Int] = []
return {
if numbers.isEmpty {
numbers = Array(min...max)
}
let index = Int(arc4random_uniform(UInt32(numbers.count)))
return numbers.remove(at: index)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Every-time you click a button, view redraws itself and thus your randomArray is created again
You need to create a Struct here which holds the property whether the
button is selected or not.
Give it a try this way:
struct concentrationGame: Hashable {
var id = UUID()
var number: Int = 0
var isSelected: Bool = false
}
class RandomNumbers: ObservableObject {
#Published var randomNumbers : [concentrationGame]
init() {
self.randomNumbers = (1...100).map{_ in arc4random_uniform(100)}.map({ concentrationGame(number: Int($0)) })
}
}
struct ContentView: View {
#ObservedObject var randomNumbers = RandomNumbers()
private var gridItemLayout = [GridItem(.adaptive(minimum: 50))]
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach($randomNumbers.randomNumbers, id: \.self) { $randomNumbers in
Button(String(randomNumbers.number)) { randomNumbers.isSelected.toggle() }
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 50)
.foregroundColor(.white)
.background( randomNumbers.isSelected ? .white : .black)
.cornerRadius(10)
}
}
}
.padding()
.ignoresSafeArea(edges: .bottom)
.navigationTitle("Concentration Grid")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI Custom View repeat forever animation show as unexpected

I created a custom LoadingView as a Indicator for loading objects from internet. When add it to NavigationView, it shows like this
enter image description here
I only want it showing in the middle of screen rather than move from top left corner
Here is my Code
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.primaryDota, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
and my content view
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
.frame(width: 30, height: 30)
}
}
}
This looks like a bug of NavigationView: without it animation works totally fine. And it wan't fixed in iOS15.
Working solution is waiting one layout cycle using DispatchQueue.main.async before string animation:
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.red, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
}
This is a bug from NavigationView, I tried to kill all possible animation but NavigationView ignored all my try, NavigationView add an internal animation to children! here all we can do right now!
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
}
}
}
struct LoadingView: View {
#State private var isLoading: Bool = Bool()
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.blue, lineWidth: 5.0)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
.onAppear { DispatchQueue.main.async { isLoading.toggle() } }
}
}

View don't disappear with the way it should

I am trying to make the yellow views disappear from bottom and top with a nice and smooth animation. (slide/move to top/bottom + fadding and keep the middle view taking the whole space)
This is my current state, and it is everything but smooth and nice haha. but it works.
import SwiftUI
struct ContentView: View {
#State var isInterfaceHidden: Bool = false
var body: some View {
VStack(spacing: 0, content: {
if !isInterfaceHidden {
Rectangle()
.id("animation")
.foregroundColor(Color.yellow)
.frame(height: 40)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity).animation(.linear))
}
Rectangle()
.id("animation2")
.foregroundColor(Color.red)
.transition(AnyTransition.opacity.animation(Animation.linear))
/// We make sure it won't cover the top and bottom view.
.zIndex(-1)
.background(Color.red)
.onTapGesture(perform: {
DispatchQueue.main.async {
self.isInterfaceHidden.toggle()
}
})
if !isInterfaceHidden {
Rectangle()
.id("animation3")
.foregroundColor(Color.yellow)
.frame(height: 80)
.transition(AnyTransition.move(edge: .bottom).combined(with: .opacity).animation(.linear))
}
})
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
Current animation:
Edit3: Thanks to #Andrew I was able to progress in a better state.
But now I have a sort of jerky animation.
Any thoughts?
I may have found a solution for you:
import SwiftUI
struct ContentView: View {
#State var isInterfaceHidden: Bool = false
var body: some View {
VStack(spacing: 0, content: {
if !isInterfaceHidden {
Rectangle()
.id("animation")
.foregroundColor(Color.yellow)
.frame(height: 40)
.transition(.topViewTransition)
}
Rectangle()
.id("animation2")
.foregroundColor(Color.red)
/// We make sure it won't cover the top and bottom view.
.zIndex(-1)
.background(Color.red)
.onTapGesture(perform: {
DispatchQueue.main.async {
self.isInterfaceHidden.toggle()
}
})
if !isInterfaceHidden {
Rectangle()
.id("animation3")
.foregroundColor(Color.yellow)
.frame(height: 80)
.transition(.bottomViewTransition)
}
})
.navigationBarTitle("")
.navigationBarHidden(true)
.animation(.easeInOut)
}
}
extension AnyTransition {
static var topViewTransition: AnyTransition {
let transition = AnyTransition.move(edge: .top)
.combined(with: .opacity)
return transition
}
static var bottomViewTransition: AnyTransition {
let transition = AnyTransition.move(edge: .bottom)
.combined(with: .opacity)
return transition
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Simply set Z index for the both of your yellow views anything higher than the default value of 1.0. This way SwiftUI will make sure they won't be covered by the red view.
The modifier to do that is .zIndex()
A simpler solution
struct Test: View {
#State private var isHiding = false
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.yellow)
.frame(width: 200, height: 100)
Rectangle()
.foregroundColor(.red)
.frame(width: 200, height: isHiding ? 100 : 80)
.onTapGesture {
withAnimation {
self.isHiding.toggle()
}
}
}
}
}

SwiftUI automatically sizing bottom sheet

There are a lot of examples of bottom sheet out there for SwiftUI, however they all specify some type of maximum height the sheet can grow to using a GeometryReader. What I would like is to create a bottom sheet that becomes only as tall as the content within it. I've come up with the solution below using preference keys, but there must be a better solution. Perhaps using some type of dynamic scrollView is the solution?
struct ContentView: View{
#State private var offset: CGFloat = 0
#State private var size: CGSize = .zero
var body: some View{
ZStack(alignment:.bottom){
VStack{
Button(offset == 0 ? "Hide" : "Show"){
withAnimation(.linear(duration: 0.2)){
if offset == 0{
offset = size.height
} else {
offset = 0
}
}
}
.animation(nil)
.padding()
.font(.largeTitle)
Spacer()
}
BottomView(offset: $offset, size: $size)
}.edgesIgnoringSafeArea(.all)
}
}
struct BottomView: View{
#Binding var offset: CGFloat
#Binding var size: CGSize
var body: some View{
VStack(spacing: 0){
ForEach(0..<5){ value in
Rectangle()
.fill(value.isMultiple(of: 2) ? Color.blue : Color.red)
.frame(height: 100)
}
}
.offset(x: 0, y: offset)
.getSize{
size = $0
offset = $0.height
}
}
}
struct SizePreferenceKey: PreferenceKey {
struct SizePreferenceData {
let bounds: Anchor<CGRect>
}
static var defaultValue: [SizePreferenceData] = []
static func reduce(value: inout [SizePreferenceData], nextValue: () -> [SizePreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct SizePreferenceModifier: ViewModifier {
let onAppear: (CGSize)->Void
func body(content: Content) -> some View {
content
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { [SizePreferenceKey.SizePreferenceData( bounds: $0)] })
.backgroundPreferenceValue(SizePreferenceKey.self) { preferences in
GeometryReader { geo in
Color.clear
.onAppear{
let size = CGSize(width: geo.size.width, height: geo.size.height)
onAppear(size)
}
}
}
}
}
extension View{
func getSize(_ onAppear: #escaping (CGSize)->Void) -> some View {
return self.modifier(SizePreferenceModifier(onAppear: onAppear))
}
}
Talk about over engineering the problem. All you have to do is specify a height of 0 if you want the sheet to be hidden, and not specify a height when it's shown. Additionally set the frame alignment to be top.
struct ContentView: View{
#State private var hide = false
var body: some View{
ZStack(alignment: .bottom){
Color.blue
.overlay(
Text("Is hidden : \(hide.description)").foregroundColor(.white)
.padding(.bottom, 200)
)
.onTapGesture{
hide.toggle()
}
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
.layoutPriority(2)
}
}
.layoutPriority(1)
.frame(height: hide ? 0 : nil, alignment: .top)
.animation(.linear(duration: 0.2))
}.edgesIgnoringSafeArea(.all)
}
}
My approach is SwiftUI Sheet based solution feel free to check the gist
you just need to add the modifier to the view and let iOS do the rest for you, no need to re-do the math ;)
Plus you will have the sheet native behavior (swipe to dismiss) and i added "tap elsewhere" to dismiss.
struct ContentView: View {
#State var activeSheet: Bool = false
#State var activeBottomSheet: Bool = false
var body: some View {
VStack(spacing: 16){
Button {
activeSheet.toggle()
} label: {
HStack {
Text("Activate Normal sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
Button {
activeBottomSheet.toggle()
} label: {
HStack {
Text("Activate Bottom sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
}
.sheet(isPresented: $activeSheet) {
// Regular sheet
sheetView
}
.sheet(isPresented: $activeBottomSheet) {
// Responsive sheet
sheetView
.asResponsiveSheet()
}
}
var sheetView: some View {
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
}
}
}
iPhone:
iPad :