Search code examples
iosswiftswiftui

iOS SwiftUI - frame size ignored when trying to resize GIF image


import SwiftUI
import PhotosUI
import AVKit
import Swime
import WebKit

struct GifImage: UIViewRepresentable {
    enum URLType {
        case local(String)
        case remote(URL?)

        var url: URL? {
            switch self {
                case .local(let name):
                return Bundle.main.url(forResource: name, withExtension: "gif")
                
                case .remote(let url):
                return url
            }
        }
    }

    let type: URLType
    var loaded: ((UIImageView, UIImage?) -> Void)?

    func makeUIView(context: Context) -> UIView {
        
        let view = UIImageView()
        DispatchQueue.global(qos: .background).async {
            
            if let url = type.url, let data = try? Data(contentsOf: url) {
                DispatchQueue.main.async {
                    if let image = UIImage.gif(data: data) {
                        view.image = image
                        view.contentMode = .scaleAspectFit
                        self.loaded?(view, image)
                    }
                }
            } else {
                DispatchQueue.main.async {
                    self.loaded?(view, nil)
                }
            }
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}


struct VidPlay: View {
    
    @State var gifSize: CGSize?
    @State var gifHeight: CGFloat = 80

    var body: some View {
        
        VStack(spacing: 20){
            

            ZStack{

                GifImage(type: .remote(URL(string: "https://media.tenor.com/hRiPtsp-m0IAAAAM/the-simpsons-homer-simpson.gif"))) { _, image in
                  gifSize = image?.size
                }
                .frame(width: 80, height: gifHeight)

            }
            .onChange(of: gifSize) { _ in
                
                if(gifSize != nil){
                    
                    gifHeight = 80 * (gifSize!.height / gifSize!.width)
                    print(gifHeight)
                }
            }
            .frame(width: 240, height: 240)
            .background(Color.blue)

        }
    }
}

It prints out the correct gifHeight, but the frame size is getting ignored, how to fix this?


Solution

  • SwiftUI views can all choose their own sizes based on a ProposedViewSize, and they can totally choose a size that is larger than the proposal. .frame merely changes the proposed size, but the UIViewRepresentable chose a size that fits its container, as a default.

    You can implement sizeThatFits in your UIViewRepresentable:

    func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIImageView, context: Context) -> CGSize? {
        guard let imageSize = uiView.image?.size else {
            return nil
        }
        return if let width = proposal.width, let height = proposal.height {
            CGSize(width: width, height: height)
        } else if let width = proposal.width {
            CGSize(width: width, height: width * imageSize.height / imageSize.width)
        } else if let height = proposal.height {
            CGSize(width: height * imageSize.width / imageSize.height, height: height)
        } else {
            nil
        }
    }
    

    Note that I used UIImageView in the signature of sizeThatFits - you should also change the signature of makeUIView and updateUIView, replacing UIView with UIImageView too.

    Now all you need on the SwiftUI side is .frame(width: 80). You don't need to track the image's size anymore.


    Another way is to override the image view's intrinsic content size in a subclass, and use fixedSize() to force SwiftUI to use its intrinsic content size.

    class MyImageView: UIImageView {
        var size: CGSize = .zero
        
        override var intrinsicContentSize: CGSize {
            size
        }
    }
    
    struct GifImage: UIViewRepresentable {
        enum URLType {
            // ...
        }
        
        let type: URLType
        let size: CGSize
        var loaded: ((UIImageView, UIImage?) -> Void)?
        
        func makeUIView(context: Context) -> MyImageView {
            
            let view = MyImageView()
            
            DispatchQueue.global(qos: .background).async {
                // ...
            }
            return view
        }
        
        func updateUIView(_ uiView: MyImageView, context: Context) {
            uiView.size = size
            uiView.invalidateIntrinsicContentSize()
        }
    }
    
    // usage:
    let url = URL(string: "https://media.tenor.com/hRiPtsp-m0IAAAAM/the-simpsons-homer-simpson.gif")
    GifImage(type: .remote(url), size: .init(width: 80, height: gifHeight)) { _, image in
        gifSize = image?.size
    }
    .fixedSize()