SwiftUI: Using LongPressGesture to display a Pinterest like context menu - ios

I am trying create a context menu similar to Pinterest's context menu in their iOS app. Long pressing a post reveals a four button view, which while the user continues the longpress, is then able to drag and select the other buttons. Letting go of the long press will either select whichever button you are currently selecting or dismiss the menu altogether if you don't have anything selected. Please see an example of this below:
So far, I've tried something similar to Apple's documentation here:
https://developer.apple.com/documentation/swiftui/longpressgesture
But it seems that the gesture finishes as soon as it hits the minimumDuration defined in the gesture. I'd like the gesture to continue for as long as the user is holding, and end as soon as they let go.
Additionally, I am in the weeds when it comes to the dragging and selecting the other buttons. Here is my approach so far:
struct Example: View {
#GestureState var isDetectingLongPress = false
#State var completedLongPress = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
self.completedLongPress = finished
}
}
var body: some View {
HStack {
Spacer()
ZStack {
// Three button array to fan out when main button is being held
Button(action: {
// ToDo
}) {
Image(systemName: "circle.fill")
.frame(width: 70, height: 70)
.foregroundColor(.red)
}
.offset(x: self.isDetectingLongPress ? -90 : 0, y: self.isDetectingLongPress ? -90 : 0)
Button(action: {
// ToDo
}) {
Image(systemName: "circle.fill")
.frame(width: 70, height: 70)
.foregroundColor(.green)
}
.offset(x: 0, y: self.isDetectingLongPress ? -120 : 0)
Button(action: {
// ToDo
}) {
Image(systemName: "circle.fill")
.frame(width: 70, height: 70)
.foregroundColor(.blue)
}
.offset(x: self.isDetectingLongPress ? 90 : 0, y: self.isDetectingLongPress ? -90 : 0)
// Main button
Image(systemName: "largecircle.fill.circle")
.gesture(longPress)
}
Spacer()
}
}

The closest equivalent I can find is a context menu
Src: AppleDeveloper
if you press and hold, you get a similar effect.
Context Menu - Apple Devloper Documentation

Update: I found a solution that works, but it's using UIKit as I don't believe SwiftUI provides a way to do this natively. I followed the guidance from the answer to a similar question found here: https://stackoverflow.com/a/31591162/862561
It was however written in ObjC, so I translated roughly to Swift. For those who are curious, here is a simplified version of the process:
class UIControlsView: UIView {
let createButton = CreateButtonView()
let secondButton = ButtonView(color: .red)
var currentDraggedButton: ButtonView!
required init?(coder: NSCoder) {
fatalError("-")
}
init() {
super.init(frame: .zero)
self.backgroundColor = .green
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(self.longPress))
createButton.addGestureRecognizer(longPress)
createButton.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
createButton.center = CGPoint(x: UIScreen.main.bounds.size.width / 2, y: 40)
secondButton.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
secondButton.center = CGPoint(x: UIScreen.main.bounds.size.width / 2, y: 40)
self.addSubview(secondButton)
self.addSubview(createButton)
}
#objc func longPress(sender: UILongPressGestureRecognizer) {
if sender.state == .began {
print("Started Long Press")
secondButton.center.x = secondButton.center.x + 90
}
if sender.state == .changed {
let location = sender.location(in: self)
guard let superViewLocation = self.superview?.convert(location, from: self) else {
return
}
guard let view = self.superview?.hitTest(superViewLocation, with: nil) else {
return
}
if view.isKind(of: ButtonView.self) {
let touchedButton = view as! ButtonView
if self.currentDraggedButton != touchedButton {
if self.currentDraggedButton != nil {
self.currentDraggedButton.untouchedUp()
}
}
self.currentDraggedButton = touchedButton
touchedButton.isTouchedUp()
} else {
if self.currentDraggedButton != nil {
print("Unsetting currentDraggedButton")
self.currentDraggedButton.untouchedUp()
}
}
}
if sender.state == .ended {
print("Long Press Ended")
let location = sender.location(in: self)
guard let superViewLocation = self.superview?.convert(location, from: self) else {
return
}
guard let view = self.superview?.hitTest(superViewLocation, with: nil) else {
return
}
if view.isKind(of: ButtonView.self) {
let touchedButton = view as! ButtonView
touchedButton.untouchedUp()
touchedButton.tap()
self.currentDraggedButton = nil
}
secondButton.center.x = secondButton.center.x - 90
}
}

Related

How to make the line smoother with Path in SwiftUI?

When the user touches the screen, it will record the gesture as CGPoint and then display them with Path. But now the line is not smooth at the turning point. What should I do?
This is my code:
struct LineView: View {
#State var removeLine = false
#State var singleLineData = [CGPoint]()
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
Path { path in
path.addLines(singleLineData)
}
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
}
.gesture(
DragGesture()
.onChanged { state in
if removeLine {
singleLineData.removeAll()
removeLine = false
}
singleLineData.append(state.location)
}
.onEnded { _ in
removeLine = true
}
)
.frame(width: 370, height: 500)
}
}
struct LineView_Previews: PreviewProvider {
static var previews: some View {
LineView()
}
}
I'm back and I know a solution to make line smoother.
First I created a class to return Path 「PS: I learned this from Mrs. Karin Prater's youtube course("How to make a drawing app with SwiftUI 3"), Shoutout to her」
class DrawingEngine {
func createPath(for points: [CGPoint]) -> Path {
var path = Path()
if let firstPoint = points.first {
path.move(to: firstPoint)
}
for index in 1..<points.count {
let mid = calculateMidPoint(points[index-1], points[index])
path.addQuadCurve(to: mid, control: points[index - 1])
}
if let last = points.last {
path.addLine(to: last)
}
return path
}
func calculateMidPoint(_ p1: CGPoint, _ p2: CGPoint) -> CGPoint {
let newMidPoint = CGPoint(x: (p1.x+p2.x)/2, y: (p1.y+p2.y)/2)
return newMidPoint
}
}
Then I only record new state points whose linear distance from the previous point exceeds a certain value(10 or 20...)
.gesture(DragGesture()
.onChanged { state in
if removeLine {
removeLine = false
singleLineData = [CGPoint]()
}
var exceedsMinimumDistance: Bool {
return sqrt(pow((singleLineData[singleLineData.count-1].x - state.location.x), 2) + pow((singleLineData[singleLineData.count-1].y - state.location.y), 2)) > 20
}
if singleLineData.count == 0 {
singleLineData.append(state.location)
} else if exceedsMinimumDistance {
singleLineData.append(state.location)
}
}
.onEnded { _ in
removeLine = true
})
This is full code of LineView:
struct LineView: View {
#State var removeLine = false
#State var singleLineData = [CGPoint]()
let engine = DrawingEngine()
let minimumDistance: CGFloat = 20
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
if singleLineData.count != 0 {
Path { path in
path = engine.createPath(for: singleLineData)
}
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
}
}
.gesture(DragGesture()
.onChanged { state in
if removeLine {
removeLine = false
singleLineData = [CGPoint]()
}
var exceedsMinimumDistance: Bool {
return sqrt(pow((singleLineData[singleLineData.count-1].x - state.location.x), 2) + pow((singleLineData[singleLineData.count-1].y - state.location.y), 2)) > minimumDistance
}
if singleLineData.count == 0 {
singleLineData.append(state.location)
} else if exceedsMinimumDistance {
singleLineData.append(state.location)
}
}
.onEnded { _ in
removeLine = true
})
}
}
My English is not good, hope this solution could help you ;)

How to add other views to UIViewRepresentable?

As a beginner in Swift, I wanted to create a painting app where we can choose a photo to frame into a 3d frame.
I'm using SwiftUI and here is my code for the 3d scene.
import SwiftUI
import SceneKit
struct SceneView: UIViewRepresentable {
let scene = SCNScene()
func makeUIView(context: Context) -> SCNView {
// create a box
scene.rootNode.addChildNode(createBox())
scene.rootNode.addChildNode(createFrameY(ypos: 30))
scene.rootNode.addChildNode(createFrameY(ypos: -30))
scene.rootNode.addChildNode(createFrameX(xpos: 20))
scene.rootNode.addChildNode(createFrameX(xpos: -20))
// code for creating the camera and setting up lights is omitted for simplicity
// …
// retrieve the SCNView
let scnView = SCNView()
scnView.scene = scene
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// configure the view
scnView.backgroundColor = UIColor.gray
// show statistics such as fps and timing information
scnView.showsStatistics = true
}
func createBox() -> SCNNode {
let boxGeometry = SCNBox(width: 1, height: 60, length: 40, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
let material = SCNMaterial()
material.locksAmbientWithDiffuse = true;
material.isDoubleSided = false
material.diffuse.contents = "Mona"
boxGeometry.materials = [material]
box.localRotate(by: SCNQuaternion(x: 0, y: 0.7071, z: 0, w: 0.7071))
return box
}
func createFrameY(ypos: Float) -> SCNNode {
let boxGeometry = SCNBox(width: 2, height: 2, length: 42, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
box.position.y = ypos
boxGeometry.firstMaterial?.diffuse.contents = UIColor.black
box.localRotate(by: SCNQuaternion(x: 0, y: 0.7071, z: 0, w: 0.7071))
return box
}
func createFrameX(xpos: Float) -> SCNNode {
let boxGeometry = SCNBox(width: 2, height: 62, length: 2, chamferRadius: 0)
let box = SCNNode(geometry: boxGeometry)
box.name = "box"
box.position.x = xpos
boxGeometry.firstMaterial?.diffuse.contents = UIColor.black
return box
}
func removeBox() {
// check if box exists and remove it from the scene
guard let box = scene.rootNode.childNode(withName: "box", recursively: true) else { return }
box.removeFromParentNode()
}
func addBox() {
// check if box is already present, no need to add one
if scene.rootNode.childNode(withName: "box", recursively: true) != nil {
return
}
scene.rootNode.addChildNode(createBox())
}
}
#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
static var previews: some View {
SceneView()
.previewDevice("iPhone 13 Pro")
}
}
#endif
As I understood, when working with UIViewRepresentable, you cannot have other elements to your screen. So I tried to use this code to have both buttons and the Scene View
import SwiftUI
import SceneKit
struct ContentVie: View {
var body: some View {
Home()
}
}
struct ContentVie_Previews: PreviewProvider {
static var previews: some View {
ContentVie()
}
}
// Home View...
struct Home : View {
#State var models = [
Model(id: 0, name: "Earth", modelName: "Earth.usdz", details: "Earth is the third planet from the Sun and the only astronomical object known to harbor life. According to radiometric dating estimation and other evidence, Earth formed over 4.5 billion years ago. Earth's gravity interacts with other objects in space, especially the Sun and the Moon, which is Earth's only natural satellite. Earth orbits around the Sun in 365.256 solar days.")
]
#State var index = 0
var scene = SCNScene(named: "Earth.usdz")
var body: some View{
VStack{
// Going to use SceneKit Scene View....
// default is first object ie: Earth...
// Scene View Has a default Camera View...
// if you nedd custom means add there...
SceneView(scene: scene, options: [.autoenablesDefaultLighting,.allowsCameraControl])
// for user action...
// setting custom frame...
.frame(width: UIScreen.main.bounds.width , height: UIScreen.main.bounds.height / 2)
ZStack{
// Forward and backward buttons...
HStack{
Button(action: {
withAnimation{
if index > 0{
index -= 1
}
}
}, label: {
Image(systemName: "chevron.left")
.font(.system(size: 35, weight: .bold))
.opacity(index == 0 ? 0.3 : 1)
})
.disabled(index == 0 ? true : false)
Spacer(minLength: 0)
Button(action: {
withAnimation{
if index < models.count{
index += 1
}
}
}, label: {
Image(systemName: "chevron.right")
.font(.system(size: 35, weight: .bold))
// disabling button when no other data ....
.opacity(index == models.count - 1 ? 0.3 : 1)
})
.disabled(index == models.count - 1 ? true : false)
}
Text(models[index].name)
.font(.system(size: 45, weight: .bold))
}
.foregroundColor(.black)
.padding(.horizontal)
.padding(.vertical,30)
// Details....
VStack(alignment: .leading, spacing: 15, content: {
Text("About")
.font(.title2)
.fontWeight(.bold)
Text(models[index].details)
})
.padding(.horizontal)
Spacer(minLength: 0)
}
}
}
// Data Model...
struct Model : Identifiable {
var id : Int
var name : String
var modelName : String
var details : String
}
But I cannot add SCNNodes to the scene. Do you know how I could combine both to make it work?

SwiftUI view is adjusting itself near the safe area

I'm having an issue while moving a view that contains a SwiftUI view near the edges of the screen. The SwiftUI view moves itself to avoid being blocked by the safe area insets
I'm using UIKit to handle dragging the view via UIHostingController and a UIPanGestureRecognizer. Here's the code
import UIKit
class ViewController: UIViewController {
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let contentVc = UIHostingController(rootView: Content())
addChild(contentVc)
contentView = contentVc.view
let contentHeight = contentView.sizeThatFits(view.bounds.size).height
contentView.frame = CGRect(x: 0, y: 200, width: view.bounds.width, height: contentHeight)
view.addSubview(contentView)
contentVc.didMove(toParent: self)
let drag = UIPanGestureRecognizer(target: self, action: #selector(drag(_:)))
contentView.addGestureRecognizer(drag)
view.backgroundColor = .orange
}
var startingPoint = CGPoint.zero
#objc func drag(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
startingPoint = contentView.frame.origin
case .changed:
let location = gesture.translation(in: view)
contentView.frame.origin.y = startingPoint.y + location.y
default:
break
}
}
}
import SwiftUI
struct Content: View {
var body: some View {
VStack {
ForEach(0..<10) { i in
Text(String(i))
}
}
.background(Color.blue)
.padding(.vertical, 32)
.ignoresSafeArea()
}
}
What I'm expecting is the blue view to not to adjust itself vertically (in the preview the blue view's top padding is shrinking)
I found 2 ways for solving this issue:
Way1: Using UIKit Gesture!
class ViewController: UIViewController {
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let contentVc = UIHostingController(rootView: Content())
addChild(contentVc)
contentView = contentVc.view
let contentHeight = contentView.sizeThatFits(view.bounds.size).height
contentView.frame = CGRect(x: 0, y: 100, width: view.bounds.width, height: contentHeight)
contentView.backgroundColor = .clear
view.addSubview(contentView)
contentVc.didMove(toParent: self)
let drag = UIPanGestureRecognizer(target: self, action: #selector(drag(_:)))
contentView.addGestureRecognizer(drag)
view.backgroundColor = .orange
}
var startingPoint = CGPoint.zero
#objc func drag(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
startingPoint = contentView.frame.origin
case .changed:
let location = gesture.translation(in: view)
contentView.frame.origin.y = startingPoint.y + location.y
default:
break
}
}
}
import SwiftUI
struct Content: View {
var body: some View {
VStack {
ForEach(0..<10) { i in
Text(String(i))
}
}
.padding()
.background(Color.blue)
.frame(maxWidth: .infinity)
.background(Color.white)
}
}
Way2: Using SwiftUI Gesture!
class ViewController: UIViewController {
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let contentVc = UIHostingController(rootView: Content())
addChild(contentVc)
contentView = contentVc.view
contentView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
contentView.backgroundColor = .clear
view.addSubview(contentView)
contentVc.didMove(toParent: self)
view.backgroundColor = .orange
}
}
struct Content: View {
#State private var offset: CGFloat = .zero
#State private var lastOffset: CGFloat = .zero
var body: some View {
VStack {
ForEach(0..<10) { i in
Text(String(i))
}
}
.padding()
.background(Color.blue)
.frame(maxWidth: .infinity)
.background(Color.white)
.offset(y: offset)
.gesture(DragGesture(minimumDistance: .zero, coordinateSpace: .local).onChanged { value in
offset = lastOffset + value.translation.height
}
.onEnded { value in
lastOffset = lastOffset + value.translation.height
offset = lastOffset
})
}
}

Rotating a gradient based off device pitch, roll, and yaw - creating a hologram / security sticker effect

I'm trying to implement a little fun addition into an app I'm working on, and can't seem to figure out how to get the animation to work completely / correctly.
At the moment I have something that looks like this: https://i.imgur.com/95O1Wul.mp4
What I'm trying to achieve
I'm trying to create a hologram like you would see on some shiny game cards or in some bank notes or on those VOID stickers.
I'm using the x position of the device to determine whether to shift the start and end point of the gradient. I'm trying to figure out in my head how to use these values to make the gradient's start and end points shift based on the values. And if I incorporated the other axes how it would move around if the device is rotated.
As for code, at the moment I have:
View Model
final class ContentViewModel: ObservableObject {
#Published var gyroRotation = CMRotationRate()
private let manager = CMMotionManager()
private var timer: Timer?
func startGyroscope() {
if(manager.isGyroAvailable) {
manager.gyroUpdateInterval = 1.0
manager.startGyroUpdates()
self.timer = Timer(fire: Date(), interval: (1.0/60.0), repeats: true, block: { timer in
if let data = self.manager.gyroData {
self.gyroRotation = data.rotationRate
}
})
RunLoop.current.add(self.timer!, forMode: .default)
}
}
func stopGyroscope() {
if(self.timer != nil) {
self.timer?.invalidate()
self.timer = nil
self.manager.stopGyroUpdates()
}
}
}
View
struct ContentView: View {
#StateObject var cm = ContentViewModel()
var rotation: CMRotationRate { cm.gyroRotation }
let gridItems = [
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90)),
GridItem(.flexible(minimum: 40, maximum: 90))
]
var body: some View {
LazyVGrid(columns: gridItems, spacing: 30) {
ForEach(0..<60) { _ in
LinearGradient(
colors: [.red, .orange, .yellow, .blue, .purple, .green],
startPoint: rotation.x > 0 ? .leading : .trailing,
endPoint: rotation.x > 0 ? .trailing : .leading
)
.animation(.linear, value: rotation.x)
.frame(width: 50, height: 50)
.mask(
Image("logo")
.resizable()
.scaledToFit()
.opacity(0.3)
)
}
}
.padding(20)
.onAppear { cm.startGyroscope() }
}
}
Continuing on this to make it work, and I realised I didn't need the LinearGradient to animate but the Color itself:
View
struct CoreMotion: View {
// -- where the data lives
#ObservedObject var coreMotionVM = CoreMotionViewModel()
let gridItems = [
GridItem(), GridItem(), GridItem(), GridItem()
]
// -- the view
var body: some View {
LazyVGrid(columns: gridItems, spacing: 5) {
ForEach( 0 ..< 60 ) { _ in
coreMotionVM.tintColour
.animation(
.easeInOut,
value: coreMotionVM.roll
)
.frame(width: 60, height: 60)
.padding(.vertical)
.mask(
Image("logo")
.resizable()
.scaledToFit()
.opacity(0.2)
)
}
}
.padding(.horizontal)
.padding(.top)
}
}
ViewModel
final class CoreMotionViewModel: ObservableObject {
// -- what we share
#Published var roll: Double = 0.0
// -- core motion manager
private var manager: CMMotionManager
// -- initialisation
init() {
// -- create the instance
self.manager = CMMotionManager()
// -- how often to get the data
self.manager.deviceMotionUpdateInterval = 1/60
// -- start it on the main thread
self.manager.startDeviceMotionUpdates(to: .main) { motionData, error in
// -- error lets get out of here
guard error == nil else {
print(error!)
return
}
// -- update the data
if let motionData = motionData {
self.roll = motionData.attitude.roll
}
}
}
// cycle to the next point
var tintColour: Color {
switch roll {
case ..<(-1.5):
return .red
case -1.5 ..< -1.0 :
return .orange
case -1.0 ..< -0.5 :
return .yellow
case -0.5 ..< 0.5 :
return .green
case 0.5 ..< 1.0 :
return .blue
case 1.0... :
return .purple
default:
return .pink
}
}
}

How to animate images in SwiftUI, to play a frame animation

I want to animate images in SwiftUI's Image view
First, I tried creating some variables and a function to toggle the Image("imageVariable"). It changes but there is no animation even tried the withAnimation { } method
Secondly, I tried to use a UIKit view. Here, the animation works but I can't apply the resizable() modifier or a set a fixed frame
var images: [UIImage]! = [UIImage(named: "pushup001")!, UIImage(named: "pushup002")!]
let animatedImage = UIImage.animatedImage(with: images, duration: 0.5)
struct workoutAnimation: UIViewRepresentable {
func makeUIView(context: Self.Context) -> UIImageView {
return UIImageView(image: animatedImage)
}
func updateUIView(_ uiView: UIImageView, context: UIViewRepresentableContext<workoutAnimation>) {
}
}
struct WorkoutView: View {
var body: some View {
VStack {
workoutAnimation().aspectRatio(contentMode: .fit)
}
}
}
In method 1 I can change the image but not animate, while, in method 2 I can animate but not control it's size
I solved this using UIViewRepresentable protocol. Here I returned a UIView with the ImageView as it's subview. This gave me more control over the child's size, etc.
import SwiftUI
var images : [UIImage]! = [UIImage(named: "pushup001")!, UIImage(named: "pushup002")!]
let animatedImage = UIImage.animatedImage(with: images, duration: 0.5)
struct workoutAnimation: UIViewRepresentable {
func makeUIView(context: Self.Context) -> UIView {
let someView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
let someImage = UIImageView(frame: CGRect(x: 20, y: 100, width: 360, height: 180))
someImage.clipsToBounds = true
someImage.layer.cornerRadius = 20
someImage.autoresizesSubviews = true
someImage.contentMode = UIView.ContentMode.scaleAspectFill
someImage.image = animatedImage
someView.addSubview(someImage)
return someView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<workoutAnimation>) {
}
}
struct WorkoutView: View {
var body: some View {
VStack (alignment: HorizontalAlignment.center, spacing: 10) {
workoutAnimation()
Text("zzzz")
}
}
}
If you want a robust and cross-platform SwiftUI implementation for animated images, like GIF/APNG/WebP, I recommend using SDWebImageSwiftUI. This framework is based on exist success image loading framework SDWebImage and provides a SwiftUI binding.
To play the animation, use AnimatedImage view.
var body: some View {
Group {
// Network
AnimatedImage(url: URL(string: "https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"))
.onFailure(perform: { (error) in
// Error
})
}
}
in model :
var publisher : Timer?
#Published var index = 0
func startTimer() {
index = 0
publisher = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: {_ in
if self.index < count/*count of frames*/{
self.index += 1
}
else if let timer = self.publisher {
timer.invalidate()
self.publisher = nil
}
})
}
}
in view :
struct MyAnimationView : View {
let width : CGFloat
let images = (0...60).map { UIImage(named: "tile\($0)")! }
#StateObject var viewmodel : MyViewModel
var body: some View {
Image(uiImage: images[viewmodel.index])
.resizable()
.frame(width: width, height: width, alignment: .center)
}
}
I have created an image animation class that can be easily reused
import SwiftUI
struct ImageAnimated: UIViewRepresentable {
let imageSize: CGSize
let imageNames: [String]
let duration: Double = 0.5
func makeUIView(context: Self.Context) -> UIView {
let containerView = UIView(frame: CGRect(x: 0, y: 0
, width: imageSize.width, height: imageSize.height))
let animationImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
animationImageView.clipsToBounds = true
animationImageView.layer.cornerRadius = 5
animationImageView.autoresizesSubviews = true
animationImageView.contentMode = UIView.ContentMode.scaleAspectFill
var images = [UIImage]()
imageNames.forEach { imageName in
if let img = UIImage(named: imageName) {
images.append(img)
}
}
animationImageView.image = UIImage.animatedImage(with: images, duration: duration)
containerView.addSubview(animationImageView)
return containerView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ImageAnimated>) {
}
}
The way to use it:
ImageAnimated(imageSize: CGSize(width: size, height: size), imageNames: ["loading1","loading2","loading3","loading4"], duration: 0.3)
.frame(width: size, height: size, alignment: .center)

Resources