IrelDev
IrelDev

Reputation: 357

Set custom UIView frame in UIViewRepresentable SwiftUI

I'm trying to use my custom UIView in SwiftUI using UIViewRepresentable and I want my UIView to have the same size as I set in .frame() so that I can use it like this:

MyViewRepresentable()
.frame(width: 400, height: 250, alignment: .center)

For example, I can set a frame as a property:

struct MyViewRepresentable: UIViewRepresentable {
    var frame: CGRect
    func makeUIView(context: Context) -> UIView {
        let myView = MyView(frame: frame)

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

Usage:

struct ContentView: View {
    var body: some View {
        MyViewRepresentable(frame: CGRect(x: 0, y: 0, width: 400, height: 250))
            .frame(width: 400, height: 250, alignment: .center)
    }
}

It is not a solution and I wonder how to make it right.

Upvotes: 13

Views: 12328

Answers (4)

Can
Can

Reputation: 8571

On iOS 16 support was added for sizeThatFits, which is exactly how most layout views work. You can either perform whatever calculation is required inside of it, or just respond to the proposed view frame (the one passed from outside):

func sizeThatFits(_ proposal: ProposedViewSize, uiView: LPLinkView, context: Context) -> CGSize? {
    return proposal.replacingUnspecifiedDimensions()
}

And as someone else mentioned, if the represented view is not responding to it (e.g clipping out of bounds) then it must be an issue with the wrapped UIView.

Upvotes: 0

Muhammad Ali
Muhammad Ali

Reputation: 915

One easy hack if you do not now what is wrong with MyView is to add it inside a UIView and then return it ie

public func makeUIView(context _: UIViewRepresentableContext< MyViewRepresentable>) -> UIView {
    let view = UIView(frame: .zero)
    let myView = MyView(frame: frame)
    myView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(myView)
    NSLayoutConstraint.activate([
        myView.heightAnchor.constraint(equalTo: view.heightAnchor),
        myView.widthAnchor.constraint(equalTo: view.widthAnchor),
    ])
    return view
}

public func updateUIView(_: UIView, context _: UIViewRepresentableContext<MyViewRepresentable>) {}

Upvotes: 1

Asperi
Asperi

Reputation: 258345

If MyView has correct internal layout (which depends only on own bounds), then there is not needs in external additional limitation, ie

struct MyViewRepresentable: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        return MyView(frame: .zero)
    }
    func updateUIView(_ uiView: UIView, context: Context) {}
}

will be exactly sized below having 400x250 frame

struct ContentView: View {
    var body: some View {
        MyViewRepresentable()
            .frame(width: 400, height: 250, alignment: .center)
    }
}

if it is not then internal MyView layout has defects.

Upvotes: 15

om-ha
om-ha

Reputation: 3602

If Asperi's answer did not work out for you, then it's probably as they said: the internal MyView layout has defects.

To resolve this matter, you have a couple options:

Option A. Use AutoLayout Constraints within viewDidLoad

override func viewDidLoad() {
    super.viewDidLoad()

    // 1. View Hierarchy
    self.addChild(self.mySubview)
    self.view.addSubview(self.mySubview.view)
    self.mySubview.didMove(toParent: self)
    
    // 2. View AutoLayout Constraints
    self.mySubview.view.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        view.leadingAnchor.constraint(equalTo: self.mySubview.view.leadingAnchor),
        view.trailingAnchor.constraint(equalTo: self.mySubview.view.trailingAnchor),
        view.topAnchor.constraint(equalTo: self.mySubview.view.topAnchor),
        view.bottomAnchor.constraint(equalTo: self.mySubview.view.bottomAnchor)
    ])
}

Option B. Set frame manually within viewDidLayoutSubviews

Simply within your UIViewController, set subviews frames in viewDidLayoutSubviews.

override func viewDidLoad() {
    super.viewDidLoad()

    // 1. Add your subviews once in `viewDidLoad`
    self.addChild(self.mySubview)
    self.view.addSubview(self.mySubview.view)
    self.mySubview.didMove(toParent: self)
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // 2. Layout your subviews `viewDidLayoutSubviews`
    // Update subview frame size
    // Must be done in `viewDidLayoutSubviews`
    // Otherwise in `viewDidLoad` the bounds equal `UIScreen.main.bounds`, even if you used explicit frame within SwiftUI and used GeometryReader to pass the `CGSize` yourself to this UIViewController!
    let mySubviewFrame = self.view.bounds
    self.mySubview.view.frame = mySubviewFrame
}

Supplementary Resources

  • Basically you have multiple layout methods in iOS. Here they are ordered from oldest/worst to newest/best. This ordering is opinionated of course:
    • Frame-Based Layout. Manually manipulate frame and bounds properties on the UIView.
    • Auto-Resizing Masks. Under the hood, an autoResizingMask uses archaic springs and struts. See autoResizingMask, this answer, and this article.
    • AutoLayout Constraints.
    • AutoLayout StackView.
    • SwiftUI's VStack and HStack! This is only for SwiftUI and all the above applies to UIKit only.

Probably should have made a small dev article out of this.

Upvotes: 1

Related Questions