Can't Animate or Transition SwiftUI Images in Sequence - xcode

I have an app with a view where I show images taken with the device's camera (stored
with Core Data) and I want to create the effect of time lapse - say you take photos
of a flower over a period of time and want to show those images in a stream. I have
been able to do that - but I want to have some transition effects between the images
and have not been able to do so. I can apply rotation and a static scale but cannot
fade from 0 to 1 opacity, nor scale up from zero to full frame. I must be missing
something really simple. Here's the code using an array of photos instead of Core Data
for simplicity.
struct ContentView: View {
#State private var activeImageIndex = 0
#State private var startTimer = false
#State private var myAnimationBool = true
let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
let myShots = ["CoolEquipment", "FishAndChipsMedium", "FlatheadLake1", "GlacierBusMedium", "Hike", "HuckALaHuckMedium", "JohnAndRiverMedium", "MacDonaldLodgeLakesideMedium"
]
var body: some View {
GeometryReader { geo in
VStack {
Text("Sequence")
.foregroundColor(.blue)
.font(.system(size: 25))
Image(self.myShots[self.activeImageIndex])
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width - 20, height: geo.size.width - 20, alignment: .center)
.cornerRadius(20)
.shadow(radius: 10, x: 10, y: 10)
//None of these do anything
//.animation(.easeInOut(duration: 0.5))
//.animation(.easeInOut)
//.transition(.opacity)
//.transition(self.myAnimationBool ? .opacity : .slide)
//.transition(.scale)
//.transition(AnyTransition.opacity.combined(with: .slide))
//this works but is static
//.scaleEffect(self.myAnimationBool ? 0.5 : 1.0)
//this works but again is static
//.rotationEffect(.degrees(self.myAnimationBool ? 90 : 0))
.onReceive(self.timer) { t in
print("in fixed timer")
if self.startTimer {
self.activeImageIndex = (self.activeImageIndex + 1) % self.myShots.count
}
}
HStack {
Button(action: {
self.startTimer.toggle()
}) {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.frame(width: 200, height: 40)
Text(self.startTimer ? "Stop" : "Start").font(.headline)
}
}
}
.padding()
}//top VStack
}.onDisappear{ self.startTimer = false }
}
}
Any guidance would be appreciated. Xcode 11.3 (11C29)

Related

SwiftUI odd animation behavior with systemImage

I was messing around with a fun animation in SwiftUI when I ran into a weird problem involving animating changes to SwiftUI's SF symbols. Basically, I want to animate a set of expanding circles that lose opacity as they get farther out. This works fine when I animate the circles using the Circle() shape, but throws a weird error when I use Image(systemName: "circle"). Namely, it throws No symbol named 'circle' found in system symbol set and I get the dreaded "purple" error in Xcode. Why does my animation work with shapes but not with SF symbols?
Animation Code with Shapes:
struct ContentView: View {
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
#State var firstIndex: Int = 0
#State var secondIndex: Int = 10
#State var thirdIndex: Int = 20
#State var fourthIndex: Int = 30
private func changeIndex(index: Int) -> Int {
if index == 40 {
return 0
} else {
return index + 1
}
}
var body: some View {
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(index: firstIndex)
ExpandingCircle(index: secondIndex)
ExpandingCircle(index: thirdIndex)
ExpandingCircle(index: fourthIndex)
}
.onReceive(timer) { time in
withAnimation(.linear(duration: 0.25)) {
self.firstIndex = changeIndex(index: firstIndex)
self.secondIndex = changeIndex(index: secondIndex)
self.thirdIndex = changeIndex(index: thirdIndex)
self.fourthIndex = changeIndex(index: fourthIndex)
}
}
}
}
Where ExpandingCircle is defined as:
struct ExpandingCircle: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Circle()
.strokeBorder(Color.red, lineWidth: 4)
.frame(width: getSize(), height: getSize())
.opacity(getOpacity())
}
}
To replicate the error, swap out ExpandingCircle in ContentView for ExpandingCircleImage:
struct ExpandingCircleImage: View {
let index: Int
private func getSize() -> CGFloat {
return CGFloat(index * 2)
}
private func getOpacity() -> CGFloat {
if index == 0 || index == 40 {
return 0
} else {
return CGFloat(1 - (Double(index) * 0.025))
}
}
var body: some View {
Image(systemName: "circle")
.foregroundColor(.red)
.font(.system(size: getSize()))
.opacity(getOpacity())
}
}
Your ExpandingCircleImage is choking because you can't have a system font of size 0, and you keep trying to feed 0 to your ExpandingCircleImage view. However, in addition to that, you don't need to use a timer to drive the animation. In fact, it makes the animation look weird because a timer is not exact. Next, your ExpandingCircle or ExpandingCircleImage should animate itself and be the complete effect.
The next issue you will encounter when you fix the font size = 0 issue, is that .font(.system(size:)) is not animatable as it is. You need to write an AnimatableModifier for it. That looks like this:
struct AnimatableSfSymbolFontModifier: AnimatableModifier {
var size: CGFloat
var animatableData: CGFloat {
get { size }
set { size = newValue }
}
func body(content: Content) -> some View {
content
.font(.system(size: size))
}
}
extension View {
func animateSfSymbol(size: CGFloat) -> some View {
self.modifier(AnimatableSfSymbolFontModifier(size: size))
}
}
The animatableData variable is the key. It teaches SwiftUI what to change to render the animation. In this case, we are animating the size of the font. The view extension is just a convenience so we can use . notation.
Another trick to animating a view like this is to have multiple animations that only go part of the way of the whole. In other words, if you use four circles, the first goes to 25%, the next from 25% to 50%, then 50% to 75%, lastly 75% to 100%. You also appear to have wanted the rings to fade as the expand, so I wrote that in as well. The code below will have two animating views, one made with a shape, and one with an SF Symbol.
struct ContentView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircle(maxSize: 100)
}
.frame(height: 100)
Spacer()
ZStack {
Circle()
.foregroundColor(.black)
.frame(width: 10, height: 10)
ExpandingCircleImage(maxSize: 100)
}
.frame(height: 100)
Spacer()
}
}
}
struct ExpandingCircle: View {
let maxSize: CGFloat
#State private var animate = false
var body: some View {
ZStack {
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.75 : 1)
.scaleEffect(animate ? 0.25 : 0)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.5 : 0.75)
.scaleEffect(animate ? 0.5 : 0.25)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0.25 : 0.5)
.scaleEffect(animate ? 0.75 : 0.5)
Circle()
.strokeBorder(Color.red, lineWidth: 8)
.opacity(animate ? 0 : 0.25)
.scaleEffect(animate ? 1 : 0.75)
}
.frame(width: maxSize, height: maxSize)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
struct ExpandingCircleImage: View {
let maxSize: CGFloat
#State private var animate = false
var body: some View {
ZStack {
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.25) : 1)
.opacity(animate ? 0.75 : 1)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.5) : (maxSize * 0.25))
.opacity(animate ? 0.5 : 0.75)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize * 0.75) : (maxSize * 0.5))
.opacity(animate ? 0.25 : 0.5)
Image(systemName: "circle")
.animateSfSymbol(size: animate ? (maxSize) : (maxSize * 0.75))
.opacity(animate ? 0 : 0.25)
}
.foregroundColor(.red)
.onAppear {
withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
animate = true
}
}
}
}
Remember to include the AnimatableModifier in your code.

Calculate/Predict the future center of a frame with SwiftUI

Swift 5.2, iOS 13
I want to predict/calculate the centre point of a frame after it has been scaled so I can move a shape to it while scaling it. If I try to scale and center an view in an animation/dynamically it doesn't work, with the end result a combination of center points I suspect. So in the images below, the blue box starts in the top right hand corner, I scale and move it to the center. But as you can see from the green box, the scaling has messed up the point it needs to get to...
struct SwiftUIView2: View {
#State var relocate = Alignment.topTrailing
#State var zoom:CGFloat = 1.0
#State var tag:Bool = true
#State var tag2:Bool = false
#State var centerPoint: CGPoint = .zero
var body: some View {
ZStack {
ZStack(alignment: relocate) {
Rectangle()
.stroke(Color.blue, lineWidth: 2)
.frame(width: 32, height: 32, alignment: .center)
.onTapGesture {
withAnimation {
if self.tag {
self.zoom = 2.0
self.relocate = Alignment.center
} else {
self.relocate = Alignment.topTrailing
self.zoom = 1.0
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tag.toggle()
}
}
.scaleEffect(zoom, anchor: .topLeading)
}.frame(width: 256, height: 256, alignment: relocate)
.border(Color.red)
if !tag {
ZStack {
Rectangle()
.stroke(Color.green, lineWidth: 2)
.frame(width: 32, height: 32, alignment: .center)
.onTapGesture {
self.tag.toggle()
}
}.frame(width: 256, height: 256, alignment: .center)
.scaleEffect(zoom, anchor: .center)
}
}
}
}
GeoReader I thought would be the answer, but I get garbage from it too. Spent more than a week trying custom alignments, position, everything I can think of. Eyeballed a solution with screen site percentages for now, but obviously it doesn't work too well with different sized screens.
Here is fix (tested with Xcode 11.4 / iOS 13.4)
}
.scaleEffect(zoom) // << here !! (remove topLeading anchor)
}.frame(width: 256, height: 256, alignment: relocate)

Load image from sever inside image slider using swiftUI

I am trying to load image from server in a slide show with SwiftUi but image is not loading in the slide show. please see my code below
import SwiftUI
import Combine
struct ImageCarouselView<Content: View>: View {
private var numberOfImages: Int
private var content: Content
#State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
init(numberOfImages: Int, #ViewBuilder content: () -> Content) {
self.numberOfImages = numberOfImages
self.content = content()
}
var body: some View {
GeometryReader { geometry in
// 1
ZStack(alignment: .bottom) {
HStack(spacing: 0) {
self.content
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .leading)
.offset(x: CGFloat(self.currentIndex) * -geometry.size.width, y: 0)
.animation(.spring())
.onReceive(self.timer) { _ in
self.currentIndex = (self.currentIndex + 1) % (self.numberOfImages == 0 ? 1 : self.numberOfImages)
}
// 2
HStack(spacing: 3) {
// 3
ForEach(0..<self.numberOfImages, id: \.self) { index in
// 4
Circle()
.frame(width: index == self.currentIndex ? 10 : 8,
height: index == self.currentIndex ? 10 : 8)
.foregroundColor(index == self.currentIndex ? Color.blue : .white)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.padding(.bottom, 8)
.animation(.spring())
}
}
}
}
}
}
and in my view
struct MainView: View {
#Binding var showMenu: Bool
#State var index = 0
var body: some View {
ScrollView {
//MainView(with:"https://govcard.net/banners/banner1.jpg")
GeometryReader { geometry in
ImageCarouselView(numberOfImages: 3) {
Image("image_carousel_1")
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
Image("image_carousel_2")
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
Image("image_carousel_3")
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
}.frame(height: 200, alignment: .center)
}
i want to use url instead of asset image.
As far as I've seen the easiest way would be to add the Kingfisher Swift package to your project.import KingfisherSwiftUI to your swift file and use the KFImage(myUrl).
import KingfisherSwiftUI
struct MainView: View {
//...
KFImage(URL(string: "https://example.com/image.png")!)
}
Here are the steps to add the Kingfisher to your project:
Select File > Swift Packages > Add Package Dependency.
Enter https://github.com/onevcat/Kingfisher.git in the "Choose Package Repository" dialog.
In the next page, specify the version resolving rule as "Up to Next Major" with "5.14.0" as its earliest version.

Getting and setting the position of content in a SwiftUI ScrollView

I have a horizontal scroll view where I'd like to set a position programmatically. Here is the body of the view:
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
...
var body: some View {
GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finalmente.")
.font(Font.title)
.frame(width: scrollWidth, height: scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
}
.frame(width: viewGeometry.size.width, height: scrollHeight)
.padding([.top, .bottom], spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
Spacer(minLength: spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
How do I change this code so that I can get the current position of "The quick brown fox" and restore it at a later time? I'm just trying to do something like we've always done with contentOffset in UIKit.
I can see how a GeometryReader might be useful to get the content's current frame, but there's no equivalent writer. Setting a .position() or .offset() for the scroll view or text hasn't gotten me anywhere either.
Any help would be most appreciated!
I've been playing around with a solution and posted a Gist to what I have working in terms of programmatically setting content offsets https://gist.github.com/jfuellert/67e91df63394d7c9b713419ed8e2beb7
With the regular SwiftUI ScrollView, as far as I can tell, you can get the position with GeometryReader with proxy.frame(in: .global).minY (see your modified example below), but you cannot set the "contentOffset".
Actually if you look at the Debug View Hierarchy you will notice that our content view is embedded in an internal SwiftUI other content view to the scrollview. So you will offset vs this internal view and not the scroll one.
After searching for quite a while, I could not find any way to do it with the SwiftUI ScrollView (I guess will have to wait for Apple on this one). The best I could do (with hacks) is a scrolltobottom.
UPDATE: I previously made a mistake, as it was on the vertical scroll. Now corrected.
class SGScrollViewModel: ObservableObject{
var scrollOffset:CGFloat = 0{
didSet{
print("scrollOffset: \(scrollOffset)")
}
}
}
struct ContentView: View {
public var scrollModel:SGScrollViewModel = SGScrollViewModel()
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
var body: some View {
var topMarker:CGFloat = 0
let scrollTopMarkerView = GeometryReader { proxy -> Color in
topMarker = proxy.frame(in: .global).minX
return Color.clear
}
let scrollOffsetMarkerView = GeometryReader { proxy -> Color in
self.scrollModel.scrollOffset = proxy.frame(in: .global).minX - topMarker
return Color.clear
}
return GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: self.spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
scrollTopMarkerView.frame(height:0)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finally.")
.font(Font.title)
.frame(width: self.scrollWidth, height: self.scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
.background(scrollOffsetMarkerView)
}
.frame(width: viewGeometry.size.width, height: self.scrollHeight)
.padding([.top, .bottom], self.spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
Spacer(minLength: self.spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
}
I messed around with several solutions involving a ScrollView with one or more GeometryReaders, but ultimately I found everything easier if I just ignored ScrollView and rolled my own using View.offset(x:y:) and a DragGesture:
This allows the LinearGradient to be panned by either dragging it like a ScrollView, or by updating the binding, in the case via a Slider
struct SliderBinding: View {
#State var position = CGFloat(0.0)
#State var dragBegin: CGFloat?
var body: some View {
VStack {
Text("\(position)")
Slider(value: $position, in: 0...400)
ZStack {
LinearGradient(gradient: Gradient(colors: [.blue, .red]) , startPoint: .leading, endPoint: .trailing)
.frame(width: 800, height: 200)
.offset(x: position - 400 / 2)
}.frame(width:400)
.gesture(DragGesture()
.onChanged { gesture in
if (dragBegin == nil) {
dragBegin = self.position
} else {
position = (dragBegin ?? 0) + gesture.translation.width
}
}
.onEnded { _ in
dragBegin = nil
}
)
}
.frame(width: 400)
}
}
Clamping the drag operation to the size of the scrolled area is omitted for brevity. This code allows horizontal scrolling, use CGPoints instead of CGSize to implement it horizontally and vertically.

GeometryReader Size Calculations SwiftUI

I am attempting to use the GeometryReader to place a view. I used the GeometryReader to
modify a Rectangle() and it is placed as I expected. However, I want to programmatically
set the y value to line up with the top of the VStack frame. I thought I could read
the geometry height of the text field and use it, but I cannot assign the geometry
dimension of the textField to a variable, even though I use the same concept to move
the Rectangle.
Pictorially:
The simple code:
struct ContentView: View {
#State private var textHeight = 0.0
var body: some View {
VStack {
GeometryReader { geometry in
VStack {
Text("Title Text")
}
//self.textHeight = CGFloat(geometry.size.height)
}.border(Color.green)
GeometryReader { geometry in
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50,
y: 0, width: geometry.size.width / 2.0,
height: geometry.size.height / 2))
.fill(Color.red)
}
}.frame(width: 150, height: 300).border(Color.black)
}
}
Xcode Beta 7, Catalina Beta 7
Any guidance would be appreciated.
The problem is that you're using a VStack with two GeometryReader inside. The VStack positions its children vertically, one above the other. The y=0 of your second GeometryReader is the first y after the first GeometryReader. I suggest you change the way you're designing your UI, for example you can get the same result this way:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
VStack {
ZStack {
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50, y: 0, width: geometry.size.width / 2.0, height: geometry.size.height / 4))
.fill(Color.red)
Text("Title Text")
.frame(width: geometry.size.width, height: geometry.size.height/2.0)
.border(Color.green)
}
Spacer()
}
}
.frame(width: 150, height: 300).border(Color.black)
}
}
The result is:
To push things up and down or to the left/right you can use the really useful Spacer view.
This is my advice, but if you really need to use two GeometryReader you have to do this way:
struct ContentView2: View {
#State private var textHeight = CGFloat(0)
private func firstContentView(geometry: GeometryProxy) -> some View {
textHeight = geometry.size.height
return Text("Title Text")
.position(x: geometry.size.width/2.0, y: geometry.size.height/2.0)
}
var body: some View {
VStack(spacing: 0) {
GeometryReader { geometry in
self.firstContentView(geometry: geometry)
}.border(Color.green)
GeometryReader { geometry in
Rectangle()
.path(in: CGRect(x: geometry.size.width - 50, y: -self.textHeight, width: geometry.size.width / 2.0, height: geometry.size.height / 2))
.fill(Color.red)
}
}.frame(width: 150, height: 300).border(Color.black)
}
}
But it's a pretty dirty solution.

Resources