SwiftUI Position Overlay on Camera Preview - ios

I'm trying to add a target-like overlay to the photo preview in an app that uses
portrait on some devices and landscape on others. I can manually set coordinates but
have not been able to get the correct coordinates of the frame of the camera preview.
This is a SwiftUI application, so I am doing the camera work in a
UIViewControllerRepresentable. Here's what I have and obviously, the target circle is
always wrong when the device and/or orientation change. I don't seem to be able to capture
the frame of the modal view where the camera preview exists. I would settle to be
able to specify the frame and location of the camera preview on the modal view.
struct CaptureImageView: View {
#Binding var isShown: Bool
#Binding var image: Image?
#Binding var newUIImage: UIImage?
#Binding var showSaveButton: Bool
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, image: $newUIImage, showSaveButton: $showSaveButton)
}
}
extension CaptureImageView: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<CaptureImageView>) -> UIImagePickerController {
let vc = UIImagePickerController()
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {
vc.sourceType = .camera
vc.allowsEditing = true
vc.delegate = context.coordinator
let screenSize: CGRect = UIScreen.main.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
vc.cameraOverlayView = CircleView(frame: CGRect(x: (screenWidth / 2) - 50, y: (screenWidth / 2) + 25, width: 100, height: 100))
return vc
}
return UIImagePickerController()
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<CaptureImageView>) {
}
}
Here is the idea - but the target always needs to be centered:
And the target file:
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setLineWidth(3.0)
UIColor.red.set()
let center = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let radius = (frame.size.width - 10) / 2
context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.strokePath()
}
}
}
Any guidance would be appreciated. Xcode Version 11.3.1 (11C504)

While I would still like to find an answer for the way to get the frame of the camera - my goal could be mostly achieved by creating a circle view in a ZStack placing the camera view behind my circle view in SwiftUI like this:
if showCaptureImageView {
ZStack {
CaptureImageView(isShown: $showCaptureImageView, image: $myImage, newUIImage: $newUIImage, showSaveButton: $showSaveButton)
Circle()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10 ))
.frame(width: 100, height: 100)
.offset(CGSize(width: 0, height: -50.0))
}
}//if show

As noted by GrandSteph - a better approach would be to wrap the camera view in a Geometry Reader. Here's one approach:
if showCaptureImageView {
ZStack {
VStack {
Rectangle()//just to show that the circle below can be centered
.frame(width: 300, height: 150)
.background(Color.blue)
GeometryReader { geo in
CaptureImageView(isShown: self.$showCaptureImageView, image: self.$image)
.onAppear {
self.cameraWindowWidth = geo.size.width
self.cameraWindowHeight = geo.size.height
}
}//geo
}//v
Circle().frame(width: 50, height: 50)
.position(x: self.cameraWindowWidth / 2, y: (self.cameraWindowHeight / 2) + 150)
.foregroundColor(.red)
}//z
}//if show

Related

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.

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

SwiftUI View in UIHostingController renders incorrectly during frame animation if there's an internal animation

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()
}
}
}

ios14 Xcode 12 - Sprites do not show when touching SwiftUI objects but work when touching SKScene

I am working on a simple game to learn the new functionality of SwiftUI with SpriteKit in Xcode 12. When a user touches the SKScene touchesBegan() everything works as expected. When a user touches the Circle() gesture(DragGesture()), I call the same function, but nothing shows. I can tell from the logs that the function showFragments(location:) is getting called, but nothing shows on screen. Any ideas?
Here is the code:
import SpriteKit
import SwiftUI
struct ContentView: View {
var scene: GameScene {
let scene = GameScene()
scene.size = CGSize(width: 350, height: 600)
scene.scaleMode = .fill
scene.backgroundColor = .white
return scene
}
var body: some View {
ZStack {
SpriteView(scene: scene)
.frame(width: 350, height: 600)
.edgesIgnoringSafeArea(.all)
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
.gesture(
DragGesture(minimumDistance: 0.0)
.onChanged({ _ in
self.scene.showFragments(location: CGPoint(x: 150, y: 300))
})
)
}
}
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.showFragments(location: CGPoint(x: 150, y: 300))
}
public func showFragments(location: CGPoint) {
var i: Int = 0
while i <= 3 {
i += 1
let fragment = SKSpriteNode(texture: SKTexture(imageNamed: "Brown Armor Fragment"))
fragment.position = location
print ("fragment \(i): \(fragment.position)")
let wait = SKAction.wait(forDuration: 1)
let remove = SKAction.run({() in fragment.removeFromParent() })
fragment.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "Brown Armor Fragment"), alphaThreshold: 0.1, size: CGSize(width: 8, height: 6.5))
self.addChild(fragment)
let vx = Double.random(in: -1...1)
fragment.physicsBody?.applyImpulse(CGVector(dx: vx, dy: 0))
fragment.run(SKAction.sequence([wait, remove]))
}
}
}
You defined computable property for scene, so it creates different scene for circle gesture.
The solution is to use once-defined scene (tested with Xcode 12 / iOS 14). Use the following expression for scene:
struct ContentView: View {
let scene: GameScene = {
let scene = GameScene()
scene.size = CGSize(width: 350, height: 600)
scene.scaleMode = .fill
scene.backgroundColor = .white
return scene
}()
// ... other code
}

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