SwiftUI animation within ScrollView not working - ios

Just can't get my head around this. I have a simple animation which works perfectly. But when I wrap the view into a ScrollView (by uncommenting the 2 lines) it does not animate anymore? Anybody a clue?
import SwiftUI
struct ContentView: View {
#State var offset = CGSize(width: 0, height: 0)
var body: some View {
// ScrollView {
VStack(spacing: 50) {
Rectangle()
.frame(width: 100, height: 100)
.offset(self.offset)
Button(action: {
withAnimation {
self.offset.width += 66
}
})
{
Text("Animate me")
}
}.frame(width: 300)
// }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

I've already noticed this behaviour. The issue seems to be the explicit animation. If you go for an implicit animation it works:
struct ContentView: View {
#State var offset = CGSize(width: 0, height: 0)
var body: some View {
ScrollView {
VStack(spacing: 50) {
Rectangle()
.frame(width: 100, height: 100)
.offset(self.offset)
.animation(.linear)
Button(action: {
self.offset.width += 66
})
{
Text("Animate me")
}
}.frame(width: 300)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I haven't managed to get the reason yet, so consider this as a workaround. It may be a SwiftUI bug or something I still can't understand.

Related

Can't refresh view after changing published variable

I have three files: ContentView (main file), HomeView, ConfigView
I wish I could change one string that is located in HomeView by pressing a button in ConfigView, but I can't do it.
Edit: I realized the problem is because I'm storing objects inside the DadosTimes class. I tried to store a simple string and it worked. How can I make it work even using an object?
ContentView file:
import SwiftUI
extension View {
func inExpandingRectangle() -> some View {
ZStack {
Rectangle()
.fill(Color.clear)
self
}
}
}
//Here i have a declaration for a custom button
class Equipe {
var nome: String
var pontos: Int
var vitorias: Int
init(nome: String) {
self.nome = nome
self.pontos = 0
self.vitorias = 0
}
func addPontos(_qtd: Int){
self.pontos += _qtd
}
}
struct ContentView: View {
#EnvironmentObject var InfoJogo: DadosTimes
var body: some View {
TabView{
HomeView()
.tabItem({
Image(systemName: "house")
Text("Placar")
})
ConfigView()
.tabItem({
Image(systemName: "gear")
Text("Configurações")
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class DadosTimes: ObservableObject{
#Published var time1 = Equipe(nome: "Nós")
#Published var time2 = Equipe(nome: "Eles")
}
HomeView file:
import SwiftUI
struct HomeView: View {
#EnvironmentObject var InfoJogo: DadosTimes
var body: some View {
ZStack{
Color("FundoVerde")
.ignoresSafeArea()
VStack {
HStack{
VStack(spacing: 0){
TextField("", text: $InfoJogo.time1.nome)
.foregroundColor(Color.black)
.font(.system(size: 50))
Text(String(InfoJogo.time1.pontos))
.foregroundColor(Color.black)
.font(.system(size: 85))
Text(String(InfoJogo.time1.vitorias) + " Vitórias")
.foregroundColor(Color(.darkGray))
.font(.system(size: 17))
BotaoPrimario(title: "+", size: 50, action: {
InfoJogo.time1.addPontos(_qtd: 1)
verificaGanhador()
})
}
.inExpandingRectangle()
.fixedSize(horizontal: false, vertical: true)
VStack(spacing: 0){
Text(InfoJogo.time2.nome)
.foregroundColor(Color.black)
.font(.system(size: 50))
Text(String(InfoJogo.time2.pontos))
.foregroundColor(Color.black)
.font(.system(size: 85))
Text(String(InfoJogo.time2.vitorias) + " Vitórias")
.foregroundColor(Color(.darkGray))
.font(.system(size: 17))
BotaoPrimario(title: "+", size: 50, action: {
InfoJogo.time2.addPontos(_qtd: 1)
verificaGanhador()
})
}
.inExpandingRectangle()
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 50)
.frame(maxWidth: .infinity)
.background(Rectangle()
.foregroundColor(.white)
.cornerRadius(15)
.shadow(radius: 15)
)
}
.padding(.horizontal, 20)
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
ConfigView file
import SwiftUI
struct ConfigView: View {
#EnvironmentObject var InfoJogo: DadosTimes
var body: some View {
ZStack{
Color("FundoVerde")
.ignoresSafeArea()
VStack{
Button("Mudar Nome"){
InfoJogo.time1.nome = "oiii"
}
TextField("", text: $InfoJogo.time1.nome)
}
}
}
}
struct ConfigView_Previews: PreviewProvider {
static var previews: some View {
ConfigView()
}
}
I have already logged in console the InfoJogo.time1.nome variable and it changes in the memory. It's not updating on the screen. What should I do? I've already looked for help everywhere but I couldn't find the solution.
Sorry if it's a basic question, but I've started learning swift yesterday ;)
#Paulw11 provided a perfect description why the change doesn't trigger an update.
What you can do is trigger the update manually with objectWillChange.send().
In your button action do:
BotaoPrimario(title: "+", size: 50, action: {
infoJogo.objectWillChange.send() // here
infoJogo.time2.addPontos(_qtd: 1)
verificaGanhador()
})

SwiftUI Toolbar item getting clipped when back button is pressed

I've run in to a strange behavior in SwiftUI that I can't seem to work around.
Given the following simple example app I experience this behavior: The toolbar item renders correctly on the initial run, but navigating away and returning it gets clipped.
Sample code to recreate this:
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("Hello, world!")
.padding()
.navigationTitle("View 1")
.toolbar {
Circle()
.fill(Color.red)
.frame(width: 150, height: 150, alignment: .center)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
View2.swift
import SwiftUI
struct View2: View {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
}
}
struct View2_Previews: PreviewProvider {
static var previews: some View {
View2()
}
}
It is clipped by navigation bar as seen on view debug below so it is just rendering issue (should be clipped always):
A possible solution is to use that widget (Circle in the case) above NavigationView and align it with toolbar item.
Here is main part:
.toolbar {
Color.clear
.frame(width: 150)
.overlay(GeometryReader {
Color.clear.preference(key: ViewPointKey.self,
value: [$0.frame(in: .global).center])
})
}
//...
Circle().fill(Color.red)
.frame(width: 150, height: 150)
.position(x: pos.x, y: pos.y) // << here !!
//...
.onPreferenceChange(ViewPointKey.self) {
pos = $0.first ?? .zero
}
Complete findings and code is here

How to show the onboarding view as a sheet?

I'm trying to show my onboarding view as a sheet. Is there anyone here who can help me?
Here's how it looks right now:
I want it to be displayed as a sheet from top to bottom. I tried to do it, but it doesn't work. Is there a solution to this?
Here's the code:
Login view
import SwiftUI
struct LoginView: View {
#AppStorage("needsAppOnboarding") private var needsAppOnboarding: Bool = false
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
.sheet(isPresented: $needsAppOnboarding) {
OnboardingView()
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
OnboardingView
import SwiftUI
struct OnboardingView: View {
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("wulkanowy-svg")
.resizable()
.frame(width: 200, height: 200, alignment: .top)
.foregroundColor(Color("OnboardingColor"))
VStack(spacing: 20) {
Text("onboarding.description.title")
.font(.headline)
.multilineTextAlignment(.center)
Text("onboarding.description.content")
.font(.subheadline)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 20)
Spacer()
OnboardingButtonView()
.padding()
}
}
}
struct WulkanowyCardView_Previews: PreviewProvider {
static var previews: some View {
Group {
OnboardingView().previewLayout(.fixed(width: 320, height: 640))
}
}
}
OnboardingButtonView
import SwiftUI
struct OnboardingButtonView: View {
#AppStorage("needsAppOnboarding") var needsAppOnboarding: Bool = true
var body: some View {
Button(action: {
needsAppOnboarding = false
}, label: {
Text("onboarding.continue")
})
.padding(10)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color("OnboardingColor"))
.foregroundColor(.white)
.font(.title)
.cornerRadius(20)
}
}
struct OnboardingButtonView_Previews: PreviewProvider {
static var previews: some View {
OnboardingButtonView()
.previewLayout(.sizeThatFits)
}
}

SwiftUI: Button with offset not working in ScrollView

I'm trying to make this Profile Button to work inside a ScrollView, that when tapped, a modal view should come up. But for some reason, when I add some offset to the Button, to make it inline with the Navigation Title, the Button stops working.
Does anyone know a fix for this?
Here's my code:
import SwiftUI
struct TestView: View {
#State var showMenu = false
var body: some View {
NavigationView {
ScrollView {
VStack {
HStack {
Spacer()
Image(systemName: "person.crop.circle")
.font(.system(size: 36))
.foregroundColor(.blue)
.frame(width: 44, height: 44)
.onTapGesture {
showMenu.toggle()
}
.offset(y: -64)
}
Spacer()
}
.padding()
.sheet(isPresented: $showMenu) {
MenuView()
}
}
.navigationTitle("Browse")
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

ScrollView is not working correctly with GeometryReader

I have some issues with the scrollview and GeometryReader. I want to have a list of items under an image. And each item should have the following width and height: ((width of the entire screen - leading padding - trailing padding) / 2).
I have tried two approaches for my use case. This is the code structure of my first one:
Approach #1
ScrollView
- VStack
- Image
- GeometryReader
- ForEach
- Text
I am using the geometry reader to get the width of the VStack as it has a padding and I don't want to have the full width of the scrollview.
But with the GeometryReader, only the last item from the ForEach loop is shown on the UI. And the GeometryReader has only a small height. See screenshot.
Code:
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
var body: some View {
ScrollView {
VStack {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
GeometryReader { geometry in
ForEach(self.items()) { item in
Text(item.value)
.frame(width: geometry.size.width / CGFloat(2), height: geometry.size.width / CGFloat(2))
.background(Color.red)
}
}
.background(Color.blue)
}
.frame(maxWidth: .infinity)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The red color are the items in the ForEach loop. Blue the GeometryReader and green just the image.
Approach #2
ScrollView
-GeometryReader
- VStack
- Image
- ForEach
-Text
Then the items in my ForEach loop are rendered correctly but it's not possible to scroll anymore.
Code
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
var body: some View {
ScrollView {
GeometryReader { geometry in
VStack {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
ForEach(self.items()) { item in
Text(item.value)
.frame(width: geometry.size.width / CGFloat(2), height: geometry.size.width / CGFloat(2))
.background(Color.red)
}
}
.frame(maxWidth: .infinity)
}
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I archive to have the UI correctly shown. Am I'm missing something here?
I would really appreciate some help.
Thank you.
EDIT
I found a workaround to have the UI correctly rendered with a working scrollView but that looks quite hacky to me.
I am using a PreferenceKey for this workaround.
I am using the geometryReader inside the scrollview with a height of 0. Only to get the width of my VStack.
On preferenceKeyChange I am updating a state variable and using this for my item to set the width and height of it.
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
#State var width: CGFloat = 0
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
struct WidthPreferenceKey: PreferenceKey {
typealias Value = [CGFloat]
static var defaultValue: [CGFloat] = [0]
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}
var body: some View {
ScrollView {
VStack(spacing: 0) {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
GeometryReader { geometry in
VStack {
EmptyView()
}.preference(key: WidthPreferenceKey.self, value: [geometry.size.width])
}
.frame(height: 0)
ForEach(self.items()) { item in
Text(item.value)
.frame(width: self.width / CGFloat(2), height: self.width / CGFloat(2))
.background(Color.red)
}
}
.padding()
}
.onPreferenceChange(WidthPreferenceKey.self) { value in
self.width = value[0]
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Is this the only way of doing that, or is there a more elegant and easier way to do that?
First of all, since you are using a VStack, means you need a vertical scrolling. So, you need to set maxHeight property, instead of maxWidth.
In your first approach, you are wrapping your for..loop in a GeometryReader before wrapping it in a VStack. So, SwiftUI doesn't recognize that you want those items in a vertical stack until before building GeometryReader and its children. So, a good workaround is to put GeometryReader as the very first block before your scrollView to make it work:
import SwiftUI
struct Item: Identifiable {
var id = UUID().uuidString
var value: String
}
struct ContentView: View {
func items() -> [Item] {
var items = [Item]()
for i in 0..<100 {
items.append(Item(value: "Item #\(i)"))
}
return items
}
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
Image(systemName: "circle.fill")
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.padding()
.foregroundColor(.green)
ForEach(self.items()) { item in
Text(item.value)
.frame(width: geometry.size.width / CGFloat(2), height: geometry.size.width / CGFloat(2))
.background(Color.red)
}
}
.frame(maxHeight: .infinity)
.padding()
}
}
.background(Color.blue)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The issue with your second approach, is that you are wrapping your infinite-height VStack in a GeometryReader, which is not necessarily has an infinite height. Besides, GeometryReader is a SwiftUI block to read the dimensions from outer block, rather than getting dimensions from its children.
Generally speaking, if you want to use Geometry of current main SwiftUI component which you are making (or the device screen, if it's a fullscreen component), then put GeometryReader at a parent level of other components. Otherwise, you need to put the GeometryReader component as the unique child of a well-defined dimensions SwiftUI block.
Edited: to add padding, simply add required padding to your scrollView:
ScrollView{
//...
}
.padding([.leading,.trailing],geometry.size.width / 8)
or in just one direction:
ScrollView{
//...
}
.padding(leading,geometry.size.width / 8) // or any other value, e.g. : 30
If you need padding just for items in for loop, simply put the for loop in another VStack and add above padding there.

Resources