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)
Related
I have to implement a navigation bar where the background overflows into the content of the navigation. I would like to implement this using the UIAppearance, so that I can configure it once for all the views. I managed to do this by setting the shadowImage of the UINavigationBarAppearance, however, the content goes behind this image. Is there a way to make the content scroll in front of this image of achieve requested behaviour in a different way?
import SwiftUI
struct Overview: View {
var body: some View {
NavigationView {
ScrollView {
NavigationLink("Hello, World!") {
Text(UUID().uuidString)
}
}
.navigationTitle("Test")
}.modifier(NavigationAppearanceModifier())
}
}
struct NavigationAppearanceModifier: ViewModifier {
init() {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundImage = UIImage(color: .red)
appearance.shadowImage = UIImage(color: .red, height: 88)
appearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
func body(content: Content) -> some View {
content
}
}
extension UIImage {
convenience init(color: UIColor, width: CGFloat = 1, height: CGFloat = 1) {
let rect = CGRect(x: 0, y: 0, width: width, height: height)
UIGraphicsBeginImageContext(rect.size)
defer { UIGraphicsEndImageContext() }
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(color.cgColor)
context.fill(rect)
self.init(cgImage: context.makeImage()!)
}
}
struct Overview_Previews: PreviewProvider {
static var previews: some View {
Overview()
}
}
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
})
}
}
When animating a frame change for a UIHostingController, the rendered View within will not animate a size change if it currently has an internal animation in progress.
Is this a known issue or should I be doing something different? I have to imagine this is a pretty common use case. I've tested this on iOS 13.3 and 14.2.
Here's a screen capture of the issue: https://www.loom.com/share/91eb220dad244c848161330b574e22df
Notice that you see the yellow UIView background during the frame animation, although you should never see it. The rendered SwiftUI View immediately changes to the final frame size and only animates the origin.
import UIKit
import SwiftUI
// When `true`, issue presents itself.
let animate = true
struct SwiftUIView: View {
#State var appeared = false
let animation = Animation
.default
.repeatForever(autoreverses: false)
var body: some View {
ZStack {
Color.pink
Image(systemName: "rays")
.resizable()
.frame(width: 50, height: 50)
.rotationEffect(appeared ? .degrees(360) : .zero)
.onAppear {
withAnimation(animation) {
if animate {
appeared = true
}
}
}
}
}
}
class ViewController: UIViewController {
var vc: UIViewController!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap)))
vc = UIHostingController(rootView: SwiftUIView())
vc.view.backgroundColor = .yellow
self.addChild(vc)
view.addSubview(vc.view)
vc.view.frame = CGRect(x: 50, y: 50, width: 400, height: 100)
vc.didMove(toParent: self)
}
#objc private func didTap() {
UIView.animate(withDuration: 4) {
self.vc.view.frame = CGRect(x: 300, y: 300, width: 100, height: 400)
self.view.layoutIfNeeded()
}
}
}
I am trying to create a custom SwiftUI View but I am getting problem in updating value through ObservedObject.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var model:InfoViewModel = InfoViewModel()
var body: some View {
VStack(alignment: .leading){
CustomView(value: self.model.title)
.frame(width: 200, height: 200, alignment: .center)
}
.background(Color.green)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CustomView: UIViewRepresentable {
typealias UIViewType = UIView
var value:String
var lblTitle:UILabel = UILabel()
func makeUIView(context: Context) -> UIView {
let view:UIView = UIView()
view.addSubview(lblTitle)
lblTitle.text = value
lblTitle.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
lblTitle.backgroundColor = UIColor.red
lblTitle.textColor = UIColor.black
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
self.lblTitle.text = value
print("LBLTitle:\(self.lblTitle.text!)\nTitleValue:\(self.value)")
}
}
class InfoViewModel: ObservableObject, Identifiable {
#Published var title = "Title"
private var count = 0
init() {
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
}
#objc func fireTimer() {
count += 1
self.title = "\(self.count)"
print("Timer fired!\n\(self.title)")
}
}
Project can be download from here
Edit:
I changed code after #Asperi answer now it is updating value. but as Dávid Pásztor suggested it is not right to create #ObservedObject inside struct then how I will pass the value to CustomView with creating #ObservedObject and link that David provided is old and code in that thread now seems not working.
Here is the code that is working
struct CustomView: UIViewRepresentable {
typealias UIViewType = UIView
var value:String
func makeUIView(context: Context) -> UIView {
let view:UIView = UIView()
let lblTitle:UILabel = UILabel()
view.addSubview(lblTitle)
lblTitle.text = value
lblTitle.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
lblTitle.backgroundColor = UIColor.red
lblTitle.textColor = UIColor.black
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
for view in uiView.subviews
{
if(view.isKind(of: UILabel.self))
{
(view as! UILabel).text = value
print("LBLTitle:\((view as! UILabel).text!)\nTitleValue:\(self.value)")
}
}
}
}
You don't need to keep it as property because struct can be recreated (so you loose it), instead you have access your view via provided in arguments.
Note: UIViewRepresentable handles reference to corresponding view by itself.
struct CustomView: UIViewRepresentable {
typealias UIViewType = UILabel
var value:String
func makeUIView(context: Context) -> UILabel {
let lblTitle = UILabel()
lblTitle.text = value
lblTitle.backgroundColor = UIColor.red
lblTitle.textColor = UIColor.black
return lblTitle
}
func updateUIView(_ uiLabel: UILabel, context: Context) {
uiLabel.text = value
}
}
I have a SwiftUI app that includes a map. I want to capture an image of the map and display that image as a SwiftUI Image on another view. I have been unable to find any documentation on this. I tried two approaches at capturing and neither of them work. See the extensions below.
This is a simplified example:
ContentView:
struct ContentView: View {
#State private var showDetail: Bool = false
#State private var thumbImage: Image = Image(systemName: "gear")
var body: some View {
VStack {
Text("This is the ContentView")
if showDetail {
DetailMapView(thumbImage: $thumbImage)
}
if !showDetail {
Image(systemName: "gear")
.resizable()
.frame(width: 200, height: 200)
}
Button(action: {
self.showDetail.toggle()
}) {
Text("Tap for Map")
}
}
}
}
And the MapView:
struct DetailMapView: UIViewRepresentable {
typealias UIViewType = MKMapView
#Binding var thumbImage: Image
class Coordinator: NSObject, MKMapViewDelegate {
var parent: DetailMapView
init(_ parent: DetailMapView) {
self.parent = parent
}
}//coordinator
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.showsUserLocation = true
mapView.delegate = context.coordinator
// this does not work - it crashes
// let s = mapView.pb_takeSnapshot()
// self.thumbImage = Image(uiImage: s)
//this does not work either - it produces lots of console complaints
let t = mapView.screenshot
DispatchQueue.main.async {
self.thumbImage = Image(uiImage: t)
}
return mapView
}
}
extension UIView {
func pb_takeSnapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
drawHierarchy(in: self.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
extension UIView {
var screenshot: UIImage{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
self.layer.render(in: context)
guard let screenShot = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() };
UIGraphicsEndImageContext()
return screenShot
}
}
Console output for the screenshot version:
[VKDefault] TextureAtlasPage: Atlas page destroyed with outstanding references.: Assertion with expression - _textureRefs == 0 : Failed in file - /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/VectorKit_Sim/VectorKit-1606.34.10.29.27/src/TextureAtlas.cpp line - 604
[VKDefault] TextureAtlasPage: Atlas page destroyed with outstanding references.: Assertion with expression - _textureRefs == 0 : Failed in file - /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/VectorKit_Sim/VectorKit-1606.34.10.29.27/src/TextureAtlas.cpp line - 604
Any guidance would be appreciated. Xcode 11.4 (11E146)
For others:
This works for me - forget the extensions above. The key is to use
mapViewDidFinishRenderingMap and that function needs to be inside the Coordinator class.
func mapViewDidFinishRenderingMap(_ mapView: MKMapView, fullyRendered: Bool) {
//setup whatever region you want to see :mapView.setRegion(region, animated: true)
let render = UIGraphicsImageRenderer(size: mapView.bounds.size)
let ratio = mapView.bounds.size.height / mapView.bounds.size.width
let img = render.image { (ctx) in
mapView.drawHierarchy(in: CGRect(x: 100, y: 100, width: 300, height: 300 * ratio), afterScreenUpdates: true)
}
DispatchQueue.main.async {
self.parent.thumbImage = Image(uiImage: img)
}
}
i would recommend to use MKMapSnapshotter from Apple because if you render the map manually yourself you always get the Apple symbol with it...which you "normally" do not want ;)