i have a Shake annimation extension like this,
import SwiftUI
struct WRShake: GeometryEffect {
var amount: CGFloat = 10
var shakesPerUnit = 3
var animatableData: CGFloat
func effectValue(size: CGSize) -> ProjectionTransform {
ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)), y: 0))
}
}
extension View {
func wrshake(amount: CGFloat = 10, shakeUnits: Int = 4, animatableData: CGFloat) -> some View {
return modifier(WRShake(amount: amount, shakesPerUnit: shakeUnits, animatableData: animatableData))
}
}
then in the contentView, i got this
var body: some Scene {
WindowGroup {
NavigationView {
VStack {
Text("111")
.padding()
.background(Color.blue)
.wrshake(animatableData: self.shakeValue)
// .animation(.easeInOut(duration: 0.5))
.layoutPriority(1)
Button {
withAnimation(.easeInOut(duration: 1)) {
self.shakeValue += 1
}
// self.shakeValue += 1
} label: {
Text("shake")
}
}
}
}
then strange thing is,if i don't apply .animation(.easeInOut(duration: 0.5)) to then Text View, there is no animation.
anyone knows why?
thanks
I think because you are using explicit animations on the :App class and that class doesn't handle explicit animations.
Explicit animations is when you use the withAnimation block and leave the system to figure out how the animate.
And i suppose in #main class you did put the #State var shakeValue: CGFloat = 0 but becasuse it's a :App and not a View it doesn't change.
it works adding the .animation(.easeInOut(duration: 0.5)) because it returns a new view that handle the animations like when your extracting the view like ContentView().
(implicit animations are when you apply the modifier .animation(...))
WindowGroup {
NavigationView {
// StartUpView()
ContentView()
}
// .navigationViewStyle(StackNavigationViewStyle())
}
it turns out that, if i wrap everything into ContentView, it works, but why?
Related
I have a view with an infinite animation. These views are added to a VStack, as follows:
struct PanningImage: View {
let systemName: String
#State private var zoomPadding: CGFloat = 0
var body: some View {
VStack {
Spacer()
Image(systemName: self.systemName)
.resizable()
.aspectRatio(contentMode: .fill)
.padding(.leading, -100 * self.zoomPadding)
.frame(maxWidth: .infinity, maxHeight: 200)
.clipped()
.padding()
.border(Color.gray)
.onAppear {
let animation = Animation.linear.speed(0.5).repeatForever()
withAnimation(animation) {
self.zoomPadding = abs(sin(zoomPadding + 10))
}
}
Spacer()
}
.padding()
}
}
struct ContentView: View {
#State private var imageNames: [String] = []
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(self.imageNames, id: \.self) { imageName in
PanningImage(systemName: imageName)
}
// Please uncomment to see the problem
// .animation(.default)
// .transition(.move(edge: .top))
}
}
.toolbar(content: {
Button("Add") {
self.imageNames.append("photo")
}
})
}
}
}
Observe how adding a row to the VStack can be animated, by uncommenting the lines in ContentView.
The problem is that if an insertion into the list is animated, the "local" infinite animation no longer works correctly. My guess is that the ForEach animation is applied to each child view, and somehow these animations influence each other. How can I make both animations work?
The issue is using the deprecated form of .animation(). Be careful ignoring deprecation warnings. While often they are deprecated in favor of a new API that works better, etc. This is a case where the old version was and is, broken. And what you are seeing is as a result of this. The fix is simple, either use withAnimation() or .animation(_:value:) instead, just as the warning states. An example of this is:
struct ContentView: View {
#State private var imageNames: [String] = []
#State var isAnimating = false // You need another #State var
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(self.imageNames, id: \.self) { imageName in
PanningImage(systemName: imageName)
}
// Please uncomment to see the problem
.animation(.default, value: isAnimating) // Use isAnimating
.transition(.move(edge: .top))
}
}
.toolbar(content: {
Button("Add") {
imageNames.append("photo")
isAnimating = true // change isAnimating here
}
})
}
}
}
The old form of .animation() had some very strange side effects. This was one.
I have a #State var opacity at the App level (i.e. not in a View). I would like to animate its changes. The changes do get applied (causing re-render), but it doesn't animate. I could only get animation to work when I moved that state var into the view.
Is there a way to get withAnimation to work with App state?
My reason for storing that state at the App level is, I also want to pass it to the Settings scene.
This does not work:
#main
struct MacosPlaygroundApp: App {
#State private var opacity: Double = 1.0
var body: some Scene {
WindowGroup {
ContentView(opacity: opacity, onClick: {
withAnimation(.linear(duration: 1)) {
opacity -= 0.5
}
})
}
}
}
struct ContentView: View {
let opacity: Double
let onClick: () -> Void
var body: some View {
Button("Press here") {
onClick()
}
.padding()
.opacity(opacity)
}
}
This works:
The opacity-change animates for both the button in ContentView and the shape in ChildView.
#main
struct MacosPlaygroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State private var opacity: Double = 1.0
var body: some View {
VStack {
Button("Press here") {
withAnimation(.linear(duration: 1)) {
opacity -= 0.5
}
}
ChildView(opacity: opacity)
}
.padding()
.opacity(opacity)
}
}
struct ChildView: View {
let opacity: Double
var body: some View {
Color.pink
.contentShape(Rectangle())
.opacity(opacity)
.frame(width: 200, height: 200)
}
}
I would like to implement a toast view in SwiftUI, I am able to do that, but after some time I want to remove the view from the stack. How can we remove current view from the stack?
Here is my code.
// Actual View
struct FormView: View {
var body: some View {
Text("Hello")
.toast() // here is the toast message presentation
}
}
// Toast View
struct ToastMessage<Content: View>: View {
#State var present: Bool = false
let contentView: Content
init( contentView: #escaping () -> Content) {
self.contentView = contentView()
}
var body: some View {
ZStack {
contentView
if present {
HStack {
Text("π You're logged in. !!")
.font(.headline)
.foregroundColor(.white)
}
.padding()
.background(Color.black.opacity(0.65))
.cornerRadius(32)
.frame(maxWidth: .infinity, alignment: .center)
.position(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.origin.y + 80)
.transition(.move(edge: .top))
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 2))
}
}
.onAppear {
withAnimation {
present = true
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
present = false
}
}
}
}
}
extension View {
func toast() -> some View {
ToastMessage(contentView: {self})
}
}
Even after 5 seconds, still, this toast view is present in the View's stack. I'm expecting the ToastMessage view should be removed from the FormView after 5 seconds.
Your help would be greatly appreciated and thanks in advance.
Right now, ToastMessage is still in the hierarchy because toast() returns ToastMessage no matter what. Then, within toast message, there's a conditional about whether or not the actual message is displayed.
If you don't want ToastMessage in the hierarchy at all, you'd need to move the #State on level up so that you can use a conditional to determine whether or not it gets displayed.
// Actual View
struct FormView: View {
#State private var showToast = true
var body: some View {
Text("Hello")
.toast(show: $showToast)
.onAppear {
withAnimation { showToast = true }
}
}
}
// Toast View
struct ToastMessage<Content: View>: View {
#Binding var present: Bool
let contentView: Content
init(present: Binding<Bool>, contentView: #escaping () -> Content) {
_present = present
self.contentView = contentView()
}
var body: some View {
ZStack {
contentView
HStack {
Text("π You're logged in. !!")
.font(.headline)
.foregroundColor(.white)
}
.padding()
.background(Color.black.opacity(0.65))
.cornerRadius(32)
.frame(maxWidth: .infinity, alignment: .center)
.position(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.origin.y + 80)
.transition(.move(edge: .top))
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 2))
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
withAnimation {
present = false
}
}
}
}
}
extension View {
#ViewBuilder func toast(show: Binding<Bool>) -> some View {
if show.wrappedValue {
ToastMessage(present: show,contentView: {self})
} else {
self
}
}
}
That being said, if you don't want to move the state up, there's not necessarily any harm in keeping ToastMessage in the hierarchy.
I have modified a custom 5 star rating view (https://swiftuirecipes.com/blog/star-rating-view-in-swiftui) to suite my needs. I use that view in several places in my app to display the current rating for a struct and to change the rating through a selectable list. The problem I have is that when I select a new value for the star rating through the NavigationLink, the underlying rating value changes, but the display does not. I have created a sample app that shows the problem and included it below.
//
// StarTestApp.swift
// StarTest
//
// Created by Ferdinand Rios on 12/20/21.
//
import SwiftUI
#main
struct StarTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct StarHolder {
var rating: Double = 3.5
}
struct ContentView: View {
#State private var starHolder = StarHolder()
var body: some View {
NavigationView {
NavigationLink {
RatingView(starHolder: $starHolder)
} label: {
HStack {
Text("Rating: \(starHolder.rating, specifier: "%.1f")")
Spacer()
StarRatingView(rating: starHolder.rating, fontSize: 15)
}
.padding()
}
.navigationTitle("Test")
}
}
}
struct RatingView: View {
#Binding var starHolder: StarHolder
var body: some View {
List {
ForEach(0..<11, id: \.self) { index in
HStack {
StarRatingView(rating: Double(index) * 0.5, fontSize: 15)
Spacer()
Image(systemName: starHolder.rating == Double(index) * 0.5 ? "checkmark" : "")
}
.contentShape(Rectangle()) //allows to tap whole area
.onTapGesture {
starHolder.rating = Double(index) * 0.5
}
}
}
.navigationBarTitle(Text("Rating"))
}
}
struct StarRatingView: View {
private static let MAX_RATING: Double = 5 // Defines upper limit of the rating
private let RATING_COLOR = Color(UIColor(red: 1.0, green: 0.714, blue: 0.0, alpha: 1.0))
private let EMPTY_COLOR = Color(UIColor.lightGray)
private let fullCount: Int
private let emptyCount: Int
private let halfFullCount: Int
let rating: Double
let fontSize: Double
init(rating: Double, fontSize: Double) {
self.rating = rating
self.fontSize = fontSize
fullCount = Int(rating)
emptyCount = Int(StarRatingView.MAX_RATING - rating)
halfFullCount = (Double(fullCount + emptyCount) < StarRatingView.MAX_RATING) ? 1 : 0
}
var body: some View {
HStack (spacing: 0.5) {
ForEach(0..<fullCount) { _ in
self.fullStar
}
ForEach(0..<halfFullCount) { _ in
self.halfFullStar
}
ForEach(0..<emptyCount) { _ in
self.emptyStar
}
}
}
private var fullStar: some View {
Image(systemName: "star.fill").foregroundColor(RATING_COLOR)
.font(.system(size: fontSize))
}
private var halfFullStar: some View {
Image(systemName: "star.lefthalf.fill").foregroundColor(RATING_COLOR)
.font(.system(size: fontSize))
}
private var emptyStar: some View {
Image(systemName: "star").foregroundColor(EMPTY_COLOR)
.font(.system(size: fontSize))
}
}
If you run the app, the initial rating will be 3.5 and the stars will show the correct rating. When you select the stars, the RatingView will display with the correct rating checked. Select another rating and return to the ContentView. The text for the rating will update, but the star rating will still be the same as before.
Can anyone point me to what I am doing wrong here? I assume that the StarRatingView would refresh when the starHolder rating changes.
There are a couple of problems here. First, in your RatingView, you are passing a Binding<StarHolder>, but when you update the rating, the struct doesn't show as changed. To fix this, pass in a Binding<Double>, and the change will get noted in ContentView.
The second problem is that StarRatingView can't pick up on the change, so it needs some help. I just stuck an .id(starHolder.rating) onto StarRatingView in ContentView, and that signals to SwiftUI when the StarRatingView has changed so it is updated.
struct ContentView: View {
#State private var starHolder = StarHolder()
var body: some View {
NavigationView {
VStack {
NavigationLink {
RatingView(rating: $starHolder.rating)
} label: {
HStack {
Text("Rating: \(starHolder.rating, specifier: "%.1f")")
Spacer()
StarRatingView(rating: starHolder.rating, fontSize: 15)
.id(starHolder.rating)
}
.padding()
}
.navigationTitle("Test")
}
}
}
}
struct RatingView: View {
#Binding var rating: Double
var body: some View {
List {
ForEach(0..<11, id: \.self) { index in
HStack {
StarRatingView(rating: Double(index) * 0.5, fontSize: 15)
Spacer()
Image(systemName: rating == Double(index) * 0.5 ? "circle.fill" : "circle")
}
.contentShape(Rectangle()) //allows to tap whole area
.onTapGesture {
rating = Double(index) * 0.5
}
}
}
.navigationBarTitle(Text("Rating"))
}
}
One last thing. SwiftUI does not like the "" as an SF Symbol, so I changed the checks to "circle" and "circle.fill". Regardless, you should provide an actual image for both parts of the ternary. Or you could use a "check" and make .opacity() the ternary.
In your StarRatingView change the ForEach(0..<fullCount) {...} etc...
to ForEach(0..<fullCount, id: \.self) {...}.
Same for halfFullCount and emptyCount.
Works well for me.
Given a ScrollView like the following
If I have a ScrollView like the one below, can I display 1 to 10 again after the CircleView of 1 to 10?
I want to use the same 1-10 values and display 1, 2, 3....10 after 10. I want to use the same 1...10 values and display 1, 2, 3...10 after 10.
struct ContentView: View {
var body: some View {
VStack {
Divider()
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(0..<10) { index in
CircleView(label: "\(index)")
}
}.padding()
}.frame(height: 100)
Divider()
Spacer()
}
}
}
struct CircleView: View {
#State var label: String
var body: some View {
ZStack {
Circle()
.fill(Color.yellow)
.frame(width: 70, height: 70)
Text(label)
}
}
}
reference:
https://www.simpleswiftguide.com/how-to-create-horizontal-scroll-view-in-swiftui/
This can be done like illustrated in the Advanced ScrollView Techniques video by Apple. Although that video is from before the SwiftUI era, you can easily implement it in SwiftUI. The advantage of this technique (compared to Toto Minai's answer) is that it does not need to allocate extra memory when scrolling up or down (And you will not run out of memory when you scroll too far π).
Here is an implementation in SwiftUI.
import SwiftUI
let overlap = CGFloat(100)
struct InfiniteScrollView<Content: View>: UIViewRepresentable {
let content: Content
func makeUIView(context: Context) -> InfiniteScrollViewRenderer {
let contentWidth = CGFloat(100)
let tiledContent = content
.float(above: content) // For an implementation of these modifiers:
.float(below: content) // see https://github.com/Dev1an/SwiftUI-InfiniteScroll
let contentController = UIHostingController(rootView: tiledContent)
let contentView = contentController.view!
contentView.frame.size.height = contentView.intrinsicContentSize.height
contentView.frame.size.width = contentWidth
contentView.frame.origin.y = overlap
let scrollview = InfiniteScrollViewRenderer()
scrollview.addSubview(contentView)
scrollview.contentSize.height = contentView.intrinsicContentSize.height * 2
scrollview.contentSize.width = contentWidth
scrollview.contentOffset.y = overlap
return scrollview
}
func updateUIView(_ uiView: InfiniteScrollViewRenderer, context: Context) {}
}
class InfiniteScrollViewRenderer: UIScrollView {
override func layoutSubviews() {
super.layoutSubviews()
let halfSize = contentSize.height / 2
if contentOffset.y < overlap {
contentOffset.y += halfSize
} else if contentOffset.y > halfSize + overlap {
contentOffset.y -= halfSize
}
}
}
The main idea is to
Tile the static content. This is done using the float() modifiers.
Change the offset of the scrollview to replace the current view with a previous or next tile when you reach a bound. This is done in layoutSubviews of the InfiniteScrollViewRenderer
Drawback
The main drawback of this technique is that up until now (July 2021) Lazy Stacks don't appear to be rendered lazily when they are not inside a SwiftUI List or ScrollView.
You could use LazyHStack to add a new item when the former item appeared:
// Use `class`, since you don't want to make a copy for each new item
class Item {
var value: Int
// Other properties
init(value: Int) {
self.value = value
}
}
struct ItemWrapped: Identifiable {
let id = UUID()
var wrapped: Item
}
struct ContentView: View {
static let itemRaw = (0..<10).map { Item(value: $0) }
#State private var items = [ItemWrapped(wrapped: itemRaw.first!)]
#State private var index = 0
var body: some View {
VStack {
Divider()
// Scroll indicator might be meaningless?
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 10) {
ForEach(items) { item in
CircleView(label: item.wrapped.value.formatted())
.onAppear {
// Index iteration
index = (index + 1) % ContentView.itemRaw.count
items.append(
ItemWrapped(wrapped: ContentView.itemRaw[index]))
}
}
}.padding()
}.frame(height: 100)
Divider()
Spacer()
}
}
}
You can do it in SwiftUI but it doesnβt use a ScrollView. Try this View available here: https://gist.github.com/kevinbhayes/550e4b080d2761aa20d351ff01bab13e