SwiftUI Can't Capture Image of MapView - ios

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 ;)

Related

Background for PencilKit (such as a grid) [duplicate]

I'm learning about PencilKit.
I have a canvas, and I want to set a background image that we can draw on it.
When I save my canvas, I want my background image to be visible
But I have an error :
Cannot convert value of type 'Image' to expected argument type 'UIImage?'
Image("badmintoncourt") is an image from my assets
I can't find out how to solve it, but I maybe not in the right way to add a background image to my canvas
struct Home : View {
#State var canvas = PKCanvasView()
#Environment(\.undoManager) private var undoManager
#State var showingAlert = false
var body: some View{
NavigationView{
MyCanvas(canvasView: canvas)
.navigationTitle("Drawing")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct MyCanvas: UIViewRepresentable {
var canvasView: PKCanvasView
let picker = PKToolPicker.init()
func makeUIView(context: Context) -> PKCanvasView {
self.canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
self.canvasView.isOpaque = false
self.canvasView.backgroundColor = UIColor.clear
self.canvasView.becomeFirstResponder()
let imageView = Image("badmintoncourt")
let subView = self.canvasView.subviews[0]
subView.addSubview(imageView)
subView.sendSubviewToBack(imageView)
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
picker.addObserver(canvasView)
picker.setVisible(true, forFirstResponder: uiView)
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
Edit :
Here is my code to save image :
func SaveImage(){
let image = canvas.drawing.image(from: canvas.drawing.bounds, scale: 1)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
The Image is not a UIView, you have to use UIImageView for this case
let imageView = UIImageView(image: UIImage(named: "badmintoncourt"))
let subView = self.canvasView.subviews[0]
subView.addSubview(imageView)
subView.sendSubviewToBack(imageView)
This is how I solved this problem to allow drawing over the image and saving both in one final image. This solution makes the canvas for drawing exactly the size of the image.
I add the canvas as an overlay for the Image and store the drawing in seperate image. Once I'm done with my drawing, I merge both images into one.
struct DrawOnImageView: View {
#Binding var image: UIImage
let onSave: (UIImage) -> Void
#State private var drawingOnImage: UIImage = UIImage()
#State private var canvasView: PKCanvasView = PKCanvasView()
init(image: Binding<UIImage>, onSave: #escaping (UIImage) -> Void) {
self.image = image
self.onSave = onSave
}
var body: some View {
VStack {
Button(action: { save() }, label: Text("Save"))
Image(uiImage: self.image)
.resizable()
.aspectRatio(contentMode: .fit)
.edgesIgnoringSafeArea(.all)
.overlay(CanvasView(canvasView: $canvasView, onSaved: onChanged), alignment: .bottomLeading)
}
}
private func onChanged() -> Void {
self.drawingOnImage = canvasView.drawing.image(
from: canvasView.bounds, scale: UIScreen.main.scale)
}
private func initCanvas() -> Void {
self.canvasView = PKCanvasView();
self.canvasView.isOpaque = false
self.canvasView.backgroundColor = UIColor.clear
self.canvasView.becomeFirstResponder()
}
private func save() -> Void {
onSave(self.image.mergeWith(topImage: drawingOnImage))
}
}
This extension to UIImage will allow you to merge images. I used the code from this answer How to merge two UIImages?
public extension UIImage {
func mergeWith(topImage: UIImage) -> UIImage {
let bottomImage = self
UIGraphicsBeginImageContext(size)
let areaSize = CGRect(x: 0, y: 0, width: bottomImage.size.width, height: bottomImage.size.height)
bottomImage.draw(in: areaSize)
topImage.draw(in: areaSize, blendMode: .normal, alpha: 1.0)
let mergedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return mergedImage
}
}
Finally, this is my canvas view, even though I think MyCanvas in your code should work just fine. My view is based from this PencilKit tutorial.
struct CanvasView {
#Binding var canvasView: PKCanvasView
let onSaved: () -> Void
#State var toolPicker = PKToolPicker()
}
extension CanvasView: UIViewRepresentable {
func makeUIView(context: Context) -> PKCanvasView {
canvasView.tool = PKInkingTool(.pen, color: .gray, width: 10)
#if targetEnvironment(simulator)
canvasView.drawingPolicy = .anyInput
#endif
canvasView.delegate = context.coordinator
showToolPicker()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(canvasView: $canvasView, onSaved: onSaved)
}
}
private extension CanvasView {
func showToolPicker() {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
}
class Coordinator: NSObject {
var canvasView: Binding<PKCanvasView>
let onSaved: () -> Void
init(canvasView: Binding<PKCanvasView>, onSaved: #escaping () -> Void) {
self.canvasView = canvasView
self.onSaved = onSaved
}
}
extension Coordinator: PKCanvasViewDelegate {
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if !canvasView.drawing.bounds.isEmpty {
onSaved()
}
}
}

Remove white background on MKAnnotationView

So I have an annotation that is being created using MKAnnotationView, but I am attempting to utilize SwiftUI to actually created the view instead of UIKit, but I'm having some problems.
So I have the following class which is for the MKAnnotationView:
final class LandmarkAnnotationView2: MKAnnotationView {
static let ReuseID = "landmarkAnnotation"
var place: Place?
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForDisplay() {
super.prepareForDisplay()
image = LandmarkPin(
isBrown: (place?.show ?? false)
).takeScreenshot(
origin: CGPoint(x: 0, y: 0),
size: CGSize(width: 35, height: 35)
)
}
}
This class has two helper extensions, which are these:
extension UIView {
var renderedImage: UIImage {
// rect of capure
let rect = self.bounds
// create the context of bitmap
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context: CGContext = UIGraphicsGetCurrentContext()!
self.layer.render(in: context)
// get a image from current context bitmap
let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return capturedImage
}
}
extension View {
func takeScreenshot(origin: CGPoint, size: CGSize) -> UIImage {
let window = UIWindow(frame: CGRect(origin: origin, size: size))
let hosting = UIHostingController(rootView: self)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
Now I can create my SwiftUI view called LandmarkPin() with the following code:
struct LandmarkPin: View {
var isBrown = false
var body: some View {
Image("map-pin-full-cluster-1")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35)
.clipped()
.foregroundColor(.blue)
}
}
The problem that I'm having is that there is a white background attached to the image, no matter what modifiers I pass down, as shown here:
Does anyone know what I might be doing wrong or how I can successfully call a SwiftUI View for a MKAnnotationView, but make the background transparent? The Image is a SVG, which has no background when I output it outside of MKAnnotationView.

How to convert SwiftUI View body to UIImage in ViewController

I am working on this convertion and tried many solutions (extensions and methods) as there are so many questions and answers related to this but nothing helped like I have tried following solutions but didn't helped
Tried Solutions
https://stackoverflow.com/a/64005395/15023395
https://stackoverflow.com/a/41288197/15023395
Below is taken from https://stackoverflow.com/a/59333377/12299030
extension View {
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
// locate far out of screen
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
let image = controller.view.asImage()
controller.view.removeFromSuperview()
return image
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
// rendererContext.cgContext.addPath(
// UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
// rendererContext.cgContext.clip()
// As commented by #MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
// DispatchQueue.main.async {
layer.render(in: rendererContext.cgContext)
// }
}
}
}
This solution helped but I don't want to add image as subview of superView
func extractView(){
let hostView = UIHostingController(rootView: ContentView())
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor),
]
self.view.addSubview(hostView.view)
self.view.addConstraints(constraints)
}
What I want to do ???
I have a struct which extends swiftUI View and I have a design in it. Now I want to convert that swiftUI View into UIImage inside ViewController of storyboard that when my screen loads and viewDidLoad() function calls then system updates image of UIImageView in story board
Here is my SwiftUI code
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack(alignment: .center){
Rectangle()
.frame(width: 200, height: 75)
.cornerRadius(10)
.foregroundColor(.white)
Circle()
.stroke(lineWidth:5)
.foregroundColor(.red)
.frame(width: 75, height: 75, alignment: .leading)
.background(
Image("tempimage")
.resizable()
)
}
}
}
You can do this... but not in viewDidLoad() -- you have to wait at least until viewDidLayoutSubviews().
And, the view must be added to the view hierarchy -- but it can be removed as soon as we generate the image so it's never seen "on-screen."
Note: all "result" images here use:
a 240 x 200 image view
.contentMode = .center
green background so we can see the frame
and we give the UIImage generate from the SwiftUI ContentView a yellow background, because we will need to address some layout quirks.
So, to generate the image and set it to a UIImageView, we can do this:
// we will generate the image in viewDidLayoutSubview()
// but that can be (and usually is) called more than once
// so we'll use this to make sure we only generate the image once
var firstTime: Bool = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// we only want this to run once
if firstTime {
firstTime = false
if let img = imageFromContentView() {
imgView.image = img
}
}
}
using this imageFromContentView() func:
func imageFromContentView() -> UIImage? {
let swiftUIView = UIHostingController(rootView: ContentView())
// add as chlld controller
addChild(swiftUIView)
// make sure we can get its view (safely unwrap its view)
guard let v = swiftUIView.view else {
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return nil
}
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// size the view to its content
v.sizeToFit()
// force it to layout its subviews
v.setNeedsLayout()
v.layoutIfNeeded()
// if we want to see the background
v.backgroundColor = .systemYellow
// get it as a UIImage
let img = v.asImage()
// we're done with it, so get rid of it
v.removeFromSuperview()
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return img
}
Result #1:
Notice the 20-pt yellow band at the top, and the content is not vertically centered... that's because the UIHostingController applies a safe area layout guide.
Couple options to get around that...
If we add this line:
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// add same bottom safe area inset as top
swiftUIView.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: v.safeAreaInsets.top, right: 0)
// size the view to its content
v.sizeToFit()
we get this result:
the rendered image now has 20-pts Top and Bottom "safe area" insets.
If we don't want any safe area insets, we can use this extension:
// extension to remove safe area from UIHostingController
// source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: #convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
and change the first line in our func to:
let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
and we get this result:
Because the SwiftUI ContentView layout is using a zStack where its content (the "ring") exceeds its vertical bounds, the top and bottom of the ring is "clipped."
We can fix that either by changing the framing in ContentView:
or by increasing the frame height of the loaded view, like this for example:
// size the view to its content
v.sizeToFit()
// for this explicit example, the "ring" extends vertically
// outside the bounds of the zStack
// so we'll add 10-pts height
v.frame.size.height += 10.0
Here's a complete implementation (using your unmodified ContentView):
class ViewController: UIViewController {
let imgView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
imgView.contentMode = .center
imgView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imgView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's put the imageView 40-pts from Top
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
// centered horizontally
imgView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// width: 240
imgView.widthAnchor.constraint(equalToConstant: 240.0),
// height: 200
imgView.heightAnchor.constraint(equalToConstant: 200.0),
])
// show the image view background so we
// can see its frame
imgView.backgroundColor = .systemGreen
}
// we will generate the image in viewDidLayoutSubview()
// but that can be (and usually is) called more than once
// so we'll use this to make sure we only generate the image once
var firstTime: Bool = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// we only want this to run once
if firstTime {
firstTime = false
if let img = imageFromContentView() {
imgView.image = img
}
}
}
func imageFromContentView() -> UIImage? {
let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
// add as chlld controller
addChild(swiftUIView)
// make sure we can get its view (safely unwrap its view)
guard let v = swiftUIView.view else {
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return nil
}
view.addSubview(v)
swiftUIView.didMove(toParent: self)
// size the view to its content
v.sizeToFit()
// for this explicit example, the "ring" extends vertically
// outside the bounds of the zStack
// so we'll add 10-pts height
v.frame.size.height += 10.0
// force it to layout its subviews
v.setNeedsLayout()
v.layoutIfNeeded()
// if we want to see the background
v.backgroundColor = .systemYellow
// get it as a UIImage
let img = v.asImage()
// we're done with it, so get rid of it
v.removeFromSuperview()
swiftUIView.willMove(toParent: nil)
swiftUIView.removeFromParent()
return img
}
}
// extension to remove safe area from UIHostingController
// source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: #convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: frame.size)
return renderer.image { context in
layer.render(in: context.cgContext)
}
}
}

How to zoom in and out with buttons on MGLMapView using Swift?

I'm a newbie and I am trying to make a simple map project. I want to zoom in and out with buttons on the map but it's not working. I already tried using MKMapView but I can't change MGLMapView to MKMapView.
I tried to set a mglMapCamera variable in MapView and use it in ContentView but it didn't work either.
Also in MapView on this line: mglMapView = mapView
I'm getting this warning:
Modifying state during view update, this will cause undefined behavior.
MapView
#State public var mglMapView = MGLMapView()
#State public var mglMapCamera = MGLMapCamera()
func makeUIView(context: Context) -> MGLMapView {
// read the key from property list
let mapTilerKey = getMapTilerkey()
validateKey(mapTilerKey)
// Build the style url
let styleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=\(mapTilerKey)")
// create the mapview
let mapView = MGLMapView(frame: .zero, styleURL: styleURL)
mglMapView = mapView
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.logoView.isHidden = true
mapView.setCenter(
CLLocationCoordinate2D(latitude: 47.127757, longitude: 8.579139),
zoomLevel: 10,
animated: true)
mapView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0)
// use the coordinator only if you need
// to respond to the map events
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: Context) {}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MGLMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
// write your custom code which will be executed
// after map has been loaded
}
}
ContentView
var mapView = MapView()
#State var currentZoom:CGFloat = 10.0
func ZoominOutMap(level:CGFloat){
let camera = MGLMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 47.127757, longitude: 8.579139), fromEyeCoordinate: self.mapView.mglMapView.camera.centerCoordinate, eyeAltitude: 10)
self.mapView.mglMapView.setCamera(camera, animated: true)
}
Buttons in ContentView
VStack {
Button("+") {
currentZoom = currentZoom + 1
self.ZoominOutMap(level: currentZoom)
}
.frame(width: 30, height: 30)
.foregroundColor(Color.white)
.background(Color.gray)
.clipShape(Circle())
Button("-") {
currentZoom = currentZoom - 1
self.ZoominOutMap(level: currentZoom)
}
.frame(width: 30, height: 30)
.foregroundColor(Color.white)
.background(Color.gray)
.clipShape(Circle())
}
You can call a cameraUpdate for the same cameraPosition but zoom+=1 or -=1 to zoom in or out.
Then call animateCamera with that update and it should zoom in just fine.

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