UIImageView Representable not resizing - swiftui

I can't find a way to make a UIImageView wrapped in a UIViewRepresentable be sized to fit the frame. It always resizes beyond the screen no matter what content Mode or clipping or explcit framing I do. (The image dimensions are much larger than the device screen frame)
To clarify: I need to use UIImageView due to some subview positioning I have to do down the line and various other reasons.
Here's a paired down example:
struct ImageView: UIViewRepresentable {
var image: UIImage
func makeUIView(context: Context) -> some UIView {
let imageView = UIImageView()
imageView.image = image
imageView.backgroundColor = .red
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.frame = CGRect(x: 0, y: 0, width: 300, height: 400)
return imageView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
Then this is how I'm trying to implement it
struct ContentView: View {
var body: some View {
ImageView(image: UIImage(named: "full-ux-bg-image")!)
//.frame(width: 300, height: 400, alignment: .center) //This is just a test of explicit sizing
.padding()
}
}
Any ideas how to make this work? I want it to fit in the SwiftUI view without going over.

It has default constrains for content hugging/compression, to have possibility to manipulate with view externally we need to lowered those (... and never set frame for representable, just in case)
Here is fixed variant (tested with Xcode 14 / iOS 16)
func makeUIView(context: Context) -> some UIView {
let imageView = UIImageView()
imageView.image = image
imageView.backgroundColor = .red
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
imageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return imageView
}

Related

Is there an easy way to pinch to zoom and drag any View in SwiftUI?

I have been looking for a short, reusable piece of code that allows to zoom and drag any view in SwiftUI, and also to change the scale independently.
This would be the answer.
The interesting part that I add is that the scale of the zoomed View can be controled from outside via a binding property. So we don't need to depend just on the pinching gesture, but can add a double tap to get the maximum scale, return to the normal scale, or have a slider (for instance) that changes the scale as we please.
I owe the bulk of this code to jtbandes in his answer to this question.
Here you have in a single file the code of the Zoomable and Scrollable view and a Test View to show how it works:
`
import SwiftUI
let maxAllowedScale = 4.0
struct TestZoomableScrollView: View {
#State private var scale: CGFloat = 1.0
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
if scale < maxAllowedScale / 2 {
scale = maxAllowedScale
} else {
scale = 1.0
}
}
}
var body: some View {
VStack(alignment: .center) {
Spacer()
ZoomableScrollView(scale: $scale) {
Image("foto_producto")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
}
.frame(width: 300, height: 300)
.border(.black)
.gesture(doubleTapGesture)
Spacer()
Text("Change the scale")
Slider(value: $scale, in: 0.5...maxAllowedScale + 0.5)
.padding(.horizontal)
Spacer()
}
}
}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
#Binding private var scale: CGFloat
init(scale: Binding<CGFloat>, #ViewBuilder content: () -> Content) {
self._scale = scale
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = maxAllowedScale
scrollView.minimumZoomScale = 1
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.bouncesZoom = true
// Create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content), scale: $scale)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
uiView.zoomScale = scale
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var scale: CGFloat
init(hostingController: UIHostingController<Content>, scale: Binding<CGFloat>) {
self.hostingController = hostingController
self._scale = scale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
self.scale = scale
}
}
}
`
I think it's the shortest, easiest way to get the desired behaviour. Also, it works perfectly, something that I haven't found in other solutions offered here. For example, the zooming out is smooth and usually it can be jerky if you don't use this approach.
The slider hast that range to show how the minimun and maximum values are respected, in a real app the range would be 1...maxAllowedScale.
As for the double tap, the behaviour can be changed very easily depending pm what you prefer.
I attach video to show everything at once:
I hope this helps anyone who's looking for this feature.

Draw overlay on AVPlayer SwiftUI

I am building an app where I want to draw rectangles on a video. The rectangles will come from a neural network and the position will be relative to the top left corner och the image frame.
To achieve that, I need to figure out a way to draw overlays on the AVPlayer view and use the actual frame as reference.
Is there an easy way of doing it in SwiftUI (which I started to learn a couple of days ago)?
Code that I have now, this is just about drawing something the way I want it. I.e. I want to find out how to position the rectangle with respect to the image/video. The orange dot should be origin if the black rectangle is the video.
Current code:
import SwiftUI
import AVKit
struct PlayVideo: View {
var moviePath: URL?
var player: AVPlayer
init(fileName: String) {
moviePath = fileURL(for: fileName)
player = AVPlayer(url: moviePath!)
}
var body: some View {
VideoPlayer(player: player)
.scaledToFill()
.overlay(
Rectangle()
.fill(Color.blue)
.frame(width: 20, height: 20),
alignment: .center)
Button(action: {
player.rate = 0.04
}, label: {
Text("Play Slow")
})
}
}
I tried with .position(x: ,y: ), but that seems to be for the parent frame.
EDIT
I have tried a new approach:
import SwiftUI
import AVKit
var player = AVPlayer()
struct PlayVideo: View {
var moviePath: URL
init(fileName: String) {
moviePath = fileURL(for: fileName)
}
var body: some View {
PlayerView(url: moviePath)
Button(action: {
player.rate = 0.04
}, label: {
Text("Play Slow")
})
}
}
struct PlayVideo_Previews: PreviewProvider {
static var previews: some View {
PlayVideo(fileName: "path")
}
}
struct PlayerView: UIViewRepresentable {
var url_: URL
init(url: URL) {
url_ = url
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(frame: .zero, url: url_)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(frame: CGRect, url: URL) {
super.init(frame: frame)
player = AVPlayer(url: url)
player.play()
playerLayer.player = player
layer.addSublayer(playerLayer)
// Create a CGRect object which is used to render a rectangle.
let rectFrame: CGRect = CGRect(x:CGFloat(0), y:CGFloat(0), width:CGFloat(100), height:CGFloat(50))
// Create a UIView object which use above CGRect object.
let greenView = UIView(frame: rectFrame)
// Set UIView background color.
greenView.backgroundColor = UIColor.green
layer.addSublayer(greenView.layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
As you can see, there is a problem with the offset of the video and the view.
I imagine one could do a second view and place on top of the video view with the same size and position. Then one can draw on that. But the problem remains how to get size and position of the video correctly.

Resizable UITextView Has sizeThatFits That Gives Wrong Size in UIViewRepresentable

I am using a UITextView in a SwiftUI app in order to get a list of editable, multiline text fields based on this answer: How do I create a multiline TextField in SwiftUI?
I use the component in SwiftUI like this:
#State private var textHeight: CGFloat = 0
...
GrowingField(text: $subtask.text ?? "", height: $textHeight, changed:{
print("Save...")
})
.frame(height: textHeight)
The GrowingField is defined like this:
struct GrowingField: UIViewRepresentable {
#Binding var text: String
#Binding var height: CGFloat
var changed:(() -> Void)?
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.isScrollEnabled = false
textView.backgroundColor = .orange //For debugging
//Set the font size and style...
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
if uiView.text != self.text{
uiView.text = self.text
}
recalculateHeight(textView: uiView, height: $height)
}
func recalculateHeight(textView: UITextView, height: Binding<CGFloat>) {
let newSize = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if height.wrappedValue != newSize.height {
DispatchQueue.main.async {
height.wrappedValue = newSize.height
}
}
}
//Coordinator and UITextView delegates...
}
The problem I'm having is that sizeThatFits calculates the correct height at first, then replaces it with an incorrect height. If I print the newSize inside recalculateHeight() it goes like this when my view loads:
(63.0, 34.333333333333336) <!-- Right
(3.0, 143.33333333333334) <!-- Wrong
(3.0, 143.33333333333334) <!-- Wrong
I have no idea where the wrong size is coming from, and I don't know why the right one is replaced. This is how it looks with the height being way too big:
If I make a change to it, the recalculateHeight() method gets called again via textViewDidChange() and it rights itself:
This is really hacky, but if I put a timer in makeUIView(), it fixes itself as well:
//Eww, gross...
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
recalculateHeight(view: textView, height: $height)
}
Any idea how I can determine where the incorrect sizeThatFits value is coming from and how I can fix it?
It took me a long time to arrive at a solution for this. It turns out the UITextView sizing logic is good. It was a parent animation that presents my views that was causing updateUIView to fire again with in-transition UITextView size values.
By setting .animation(.none) on the parent VStack that holds all my text fields, it stopped the propagation of the animation and now it works. 🙂

Trouble to make a custom UIView aspect scale fit/fill with SwiftUI

No Public API in SwiftUI to response for the resizable modifier of View protocol. Only Image in SwiftUI could work with .resizable(). Custom UIView like UIView for GIF is not resizable now.
I use SDWebImageSwiftUI AnimatedImage, which is backing UIKit View SDAnimatedImageView. AnimatedImage is not response to .resizable(), .scaleToFit, .aspectRatio(contentMode: .fit), etc. WebImage is backing SwiftUI Image, so it's working fine.
import SwiftUI
import SDWebImageSwiftUI
struct ContentView: View {
let url = URL(string: "https://media.giphy.com/media/H62DGtBRwgbrxWXh6t/giphy.gif")!
var body: some View {
VStack {
AnimatedImage(url: url)
.scaledToFit()
.frame(width: 100, height: 100)
WebImage(url: url)
.scaledToFit()
.frame(width: 100, height: 100)
}
}
}
Not sure if it's an Apple bug. Expect custom view like SDWebImageSwiftUI AnimatedImage is responsive to SwiftUI size related modifiers like .scaledToFit().
Related issue: https://github.com/SDWebImage/SDWebImageSwiftUI/issues/3
SwiftUI uses the compression resistance priority and the content hugging priority to decide what resizing is possible.
If you want to resize a view below its intrinsic content size, you need to reduce the compression resistance priority.
Example:
func makeUIView(context: Context) -> UIView {
let imageView = UIImageView(image: UIImage(named: "yourImage")!)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
return imageView
}
This will allow you to set .frame(width:height:) to any size you want.
Finally found a solution.
Make a UIView wrapper outside of the SDAnimationImageView or UIImageView, then override layoutSubviews() set the frame of subview.
Here is full code by me.
And SDWebImageSwiftUI also release a new version which uses wrapper to solve this problem.
class ImageModel: ObservableObject {
#Published var url: URL?
#Published var contentMode: UIView.ContentMode = .scaleAspectFill
}
struct WebImage: UIViewRepresentable {
#ObservedObject var imageModel = ImageModel()
func makeUIView(context: UIViewRepresentableContext<WebImage>) -> ImageView {
let uiView = ImageView(imageModel: imageModel)
return uiView
}
func updateUIView(_ uiView: ImageView, context: UIViewRepresentableContext<WebImage>) {
uiView.imageView.sd_setImage(with: imageModel.url)
uiView.imageView.contentMode = imageModel.contentMode
}
func url(_ url: URL?) -> Self {
imageModel.url = url
return self
}
func scaledToFit() -> Self {
imageModel.contentMode = .scaleAspectFit
return self
}
func scaledToFill() -> Self {
imageModel.contentMode = .scaleAspectFill
return self
}
}
class ImageView: UIView {
let imageView = UIImageView()
init(imageModel: ImageModel) {
super.init(frame: .zero)
addSubview(imageView)
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = bounds
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Swift How to pan UIImage in UiImageView?

I have a simple test application and I want to pan an image inside its view. It will not pan or zoom and I can't see what's wrong with my code.
I have followed this tutorial but implemented it in code. I've made the image width the same as the height so I can pan without necessarily zooming.
Here is my code
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
return scrollView
}()
let zoomImageView: UIImageView = {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.image = #imageLiteral(resourceName: "lighthouse")
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let screenSize = UIScreen.main.bounds
let screenHeight = screenSize.height
scrollView.frame = CGRect(x:0, y:0, width: screenHeight, height: screenHeight)
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 3.0
zoomImageView.frame = CGRect(x:0, y:0, width: screenHeight, height: screenHeight)
scrollView.addSubview(self.zoomImageView)
view.addSubview(scrollView)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return zoomImageView
}
}
Search in your code for the term contentSize. You don't see it, do you? But the most fundamental fact about how a scroll view works is this: a scroll view without a contentSize cannot scroll (i.e. "pan" as you put it). In particular, it must have a content size larger than its own bounds size in order to be able to scroll along that axis (height or width, or both). Otherwise, there is nothing to scroll.