How to find the frame of a SwiftUI UIViewRepresentable - uikit

I'm trying to wrap a custom subclass of UILabel in UIViewRepresentable to use it in SwiftUI. I'm using .sizeToFit and printing the frame, and it looks right while it's in the wrapper:
func makeUIView(context: Context) -> CustomUILabel {
let view = CustomUILabel()
view.customProperty = model.customProperty
view.sizeToFit()
print(model.latex,view.frame.size) // this prints the correct size, how to propagate?
return view
}
but when I run this in a VStack, it draws the UIViewRepresentable with the maximum space possible.
var body: some View {
GeometryReader{ geometry in
VStack(spacing: 0){
Rectangle()
.fill(Color.red)
.frame( height: geometry.size.height/2 - 5 + self.draggedOffset.height)
Rectangle()
.fill(Color.orange)
.frame(height: 10)
custonView(model:self.model)
Spacer()
}
}
Is there a way to propagate the size of the UIView to its parent, similar to how you use preference keys on a native SwiftUI view?

It is due to use of GeometryReader
Try to use
custonView(model:self.model)
.fixedSize()

When using UIViewRepresentable, the greediness you describe is controlled using the contentHuggingPriority, just like it always was in UIKit.
So in your makeUIView function, you can do this:
// resist being made larger than the intrinsicContentSize
view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
view.setContentHuggingPriority(.defaultHigh, for: .vertical)

Related

Image resizing makes Button unclickable

I have a simple scrollview with a SDWebImage pulling in Firebase URL links to display images. For some bizarre reason, when I do not give a specific frame to my images, I can click on the button positioned in the bottom left corner. But when I give it a max height or any height at all, the button becomes unclickable. This seriously might be the weirdest SwiftUI issue I have ever seen - any help would be great.
import SwiftUI
import SDWebImageSwiftUI
struct Test2FeedView: View {
#StateObject var viewmodel = FeedViewModel()
#State var show : Bool = false
var body: some View {
ScrollView{
VStack(spacing:5) {
ForEach(viewmodel.posts){ post in
ZStack(alignment:.bottomLeading) {
WebImage(url: URL(string: post.original_posted_image))
.resizable()
.scaledToFill()
.frame(maxHeight:440)
.clipped()
Button(action: {
show.toggle()
}){
Text("PRESS ME")
}
}
}
}
}.sheet(isPresented: $show) {
Text("hey")
}
}
}
The Button becomes unclickable because the result of .clipped() is only visual — in reality, the image spans all the way 'above' the button, thus is being used for hit testing. You should use .allowsHitTesting(false) to disable that.
WebImage(url: URL(string: post.original_posted_image))
.resizable()
.scaledToFill()
.frame(maxHeight:440)
.clipped()
.allowsHitTesting(false) // <- here
try putting the .frame(maxHeight:440) just before .scaledToFill(), works for me.

Prevent transparent NSWindow with a ContentView using blendMode from flickering

The following creates a transparent NSWindow with a ContentView that uses blendMode to create a colour filter overlay effect so that everything behind the window appears blended (grey monochrome in this instance). It's working as expected except when the window is not active or being dragged in which case the ContentView flickers between normal (no blending) and blended; the ContentView is also showing dirty in some cases, i.e. when inactive ContentView is partially rendering and not fully updated.
Am I missing something in terms of ContentView life-cycle / refresh in relation to NSWindow events, is my NSWindow setup correct, or is this a potential bug? Essentially, the issue doesn't occur when blendMode isn't used, as testing with a transparent NSWindow and semi-opaque ContentView behaves normally.
I'm using Xcode 12.5.1 on Big Sur 11.6.2, and targeting 10.15
Code to reproduce using the AppKit App Delegate lifecycle template:
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.edgesIgnoringSafeArea(.top)
.blendMode(BlendMode.color)
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.isOpaque = false
window.backgroundColor = .clear
window.level = .floating
window.isMovable = true
window.isMovableByWindowBackground = true
window.titlebarAppearsTransparent = true
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
struct ContentView: View {
var body: some View {
Rectangle()
.fill(Color.black)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
UPDATE 7th March
Issue persists using Xcode 13.2.1 on Monterey 12.2.1, and targeting 12.2
UPDATE 8th March
Adding a default NSVisualEffectView background view results in much greater stability in that the view no longer flickers between opaque and transparent when the window is active and being dragged.
The only issue remaining is when switching between apps and the focus is lost which sometimes causes the view to become opaque, although refocusing the window fixes the problem.
A workaround is to enable hidesOnDeactivate on NSWindow, combined with applicationShouldHandleReopen, so the window disappears when focus is lost and the issue isn't visible to the user, but ideally the window should remain visible at all times until closed.
struct VisualEffectView: NSViewRepresentable {
func makeNSView(context: Context) -> NSVisualEffectView {
let view = NSVisualEffectView()
return view
}
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
Rectangle()
.fill(Color.black)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(VisualEffectView())
}
}
This feels a bit hackish and I'm sure someone out there with greater knowledge has a more elegant solution, but adding a Timer to the view to force a redraw solves the flickering problem completely, and would therefore appear to answer the question. Note: this method also dispenses with the need for a dummy NSVisualEffectView.
struct ContentView: View {
#State var currentDate = Date()
let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Rectangle()
.fill(.black)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Text("\(currentDate)")
.foregroundColor(.clear)
.onReceive(timer) { input in
currentDate = input
}
}
}
}

How do you render Text("Content") in SwiftUI on an iPad in Swift Playgrounds vs. Xcode on a Mac?

The above code works on a Mac, using the Xcode editor. However, the same code returns: abort() called on an iPad using Swift Playground's editor. How do I render Text() on the iPad?
I followed the directions in this video:
https://developer.apple.com/videos/play/wwdc2020/10643/
I found a similar question in Apple's developer forums, but no real solution:
https://developer.apple.com/forums/thread/667357
I tried decreasing the size, but that didn't help:
"I would assume based on the other question with Abort(), that Abort() is called if it is about to crash, and it seems that the view isn't getting the bounds of the screen in the live view, so it doesn't know how/where to render the view."
import SwiftUI
import PlaygroundSupport
struct ProgressView: View {
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 40)
.foregroundColor(.blue)
Text("25%")
}
}
}
PlaygroundPage.current.setLiveView(ProgressView().padding(150))
This is most likely a bug — I filed feedback #FB9092837. But until Apple fixes it, adding a hardcoded .frame works as a hacky fix.
.frame(width: 500, height: 500)
However, it takes a while to load... it first renders in the top-left corner, and after a couple seconds, moves to the center.
I found that if you put your ProgressView inside another container view, and set the frame there, it's much faster.
Here's the code:
import SwiftUI
import PlaygroundSupport
struct ProgressView: View {
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 40)
.foregroundColor(.blue)
Text("25%")
}
}
}
struct ContainerView: View {
var body: some View {
ProgressView()
.frame(width: 500, height: 500)
}
}
PlaygroundPage.current.setLiveView(ContainerView())

SwiftUI ScrollView adds unwanted animation automatically

I have an issue with animations within a SwiftUI ScrollView. I can reproduce it in a Playground with the code seen below. I just want to animate the opacity but it also animates the scaling. If I use a VStack instead of a ScrollView it works. But I need it to be scrollable.
Did someone experienced the same issue and could give me a quick hint?
Actual behaviour: https://giphy.com/gifs/h8DSbS1xZ9PJyHIJrY
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var showText = 0.0
var body: some View {
ScrollView {
Text("Test")
.font(.title)
.opacity(showText)
Text("Another really really long text")
.opacity(showText)
}
.frame(width: 320, height: 420)
.background(Color.red)
.onAppear {
withAnimation(Animation.easeInOut(duration: 1)) {
self.showText = 1.0
}
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Here is possible solution. Tested with Xcode 11.4 / iOS 13.4
ScrollView {
VStack {
Text("Test")
.font(.title)
Text("Another really really long text")
}
.fixedSize()
.opacity(showText)
}

Issue with Buttons in SwiftUI on MacOS

I'm using SwiftUI on MACOS
If I do this:
Button(action: { } ) {
Text("Press")
.padding()
.background(Color.blue)
}
I get this:
and the two grey areas are the ends of a tappable button.
but I would expect the button to be the shape of the blue area.
Any ideas how I can get the whole blue area to be tappable.
(I did look at using .onTapGesture but this doesn't animate the button so that you know you've tapped it.)
You can achieve the look you want by using a ButtonStyle and then specifying colors and other style attributes based on the configuration values being passed in.
It would be nice if there was a happy medium where you could inherit the default button radius, automatic width based on the text length and other attributes, but at least there is the ability to specify all the attributes and get the look you want.
Hope this helps!
import SwiftUI
struct BlueButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ? Color.blue : Color.white)
.background(configuration.isPressed ? Color.white : Color.blue)
.cornerRadius(6.0)
.padding()
}
}
struct ContentView: View {
var body: some View {
VStack {
Text("Hello World")
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(action: {
}) {
Text("Press")
.frame(maxWidth: 100, maxHeight: 24)
}
.buttonStyle(BlueButtonStyle())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Inspired by #Gene Z. Ragan 's great answer I've started with that answer and taken this a bit further:
Making the ButtonStyle a bit more flexible:
struct NiceButtonStyle: ButtonStyle {
var foregroundColor: Color
var backgroundColor: Color
var pressedColor: Color
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.headline)
.padding(10)
.foregroundColor(foregroundColor)
.background(configuration.isPressed ? pressedColor : backgroundColor)
.cornerRadius(5)
}
}
and then some sugar to make it cleaner at the call site:
extension View {
func niceButton(
foregroundColor: Color = .white,
backgroundColor: Color = .gray,
pressedColor: Color = .accentColor
) -> some View {
self.buttonStyle(
NiceButtonStyle(
foregroundColor: foregroundColor,
backgroundColor: backgroundColor,
pressedColor: pressedColor
)
)
}
}
then means we can use default colouring:
white foreground, grey background and accentedColor pressedColor
Button(action: { } ) {
Text("Button A")
}
.niceButton()
or we can customise the colours:
Button(action: { } ) {
Text("Button B has a long description")
}
.niceButton(
foregroundColor: .blue,
backgroundColor: .yellow,
pressedColor: .orange
)
And we get:
Thanks again Gene.
I raised this with Apple on the Feedback assistant to see if they had any useful thoughts.
Dialogue here:
I said:
If on MACOS with SwiftUI I do this:
Button(action: { } ) {
Text("Press")
.padding()
.background(Color.blue)
}
I get a blue box with two grey bits sticking out the sides.
Image attached.
These are the two grey areas are the ends of a tappable button. but I would expect the button to be the shape of the blue area.
The same code works fine as expected on iOS.
(I did look at using .onTapGesture but this doesn't animate the button so that you know you've tapped it.)
Apple said:
iOS and macOS have different default ButtonStyles — iOS is borderless, macOS has that standard bezel effect.
It sounds like you’re trying to create a custom ButtonStyle (and so not have any system provided chrome). So you’ll want to create that, which can apply that blue background to the label of the button, and then apply that to your simple button with Text, e.g.
Button("Press"), action {}).buttonStyle(BluePaddingButtonStyle()
This will ensure that it has the same appearance on every platform you run it on.
I said:
Hi, Thanks for the explanation.
I get what you are saying and I’m ok with the method I’ve come up with.
It still doesn’t seem right that:
Button(action: { } ) {
Text("Press")
.padding()
.background(Color.blue)
}
should produce something so odd looking.
I don’t understand why that bit of code couldn’t just produce a blue button.
As I say - it’s not a problem because I’ve worked around it but it
currently doesn’t seem intuitive.
Apple said:
The provided content inside the ViewBuilder is used as the label of the button: not the entire button. The button will still come with the surrounding background, bezel, foreground styling, etc as described by it’s ButtonStyle. So if your button needs to have a very specific appearance, then it needs to customize that style: either to the BorderlessButtonStyle (though note that still does come with a specific foreground appearance style), or to a custom ButtonStyle.
My thoughts:
This did help me understand why it shows as it does but intuitively it still seems wrong !!!
You can "force" an iOS-like behavior on macOS by adding .buttonStyle(.borderless).
Button(action: { } ) {
Text("Press")
.padding()
.background(Color.blue)
}
.buttonStyle(.borderless)
I don't think this is possible but you could try using this
Supports SPM, is build for Swift 5.1 and is lean
In Swift 5, this could be achieved with below simple code -
Button("First Button") {
print("Hello World")
}
.padding(10)
.accentColor(.yellow)
.background(Color.blue)
.cornerRadius(10)

Resources