SwiftUI UIRepresentable aspect ratio changes when device is rotated - swiftui

I have the following code
class UIViewTestClass: UIView {
override func draw(_ rect: CGRect) {
"Test".draw(at: CGPoint(x: rect.midX, y: rect.midY))
let context = UIGraphicsGetCurrentContext()
context?.addRect(rect)
context?.stroke(rect)
}
}
struct TestUIView: UIViewRepresentable {
typealias UIViewType = UIView
func makeUIView(context: Context) -> UIView {
let result = UIViewTestClass()
result.backgroundColor = UIColor(.white)
return result
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
TestUIView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
// .aspectRatio(contentMode: .fit)
.padding()
}
}
It works fine when the view is presented but when the device is rotated the letters get stretched out or squeezed. Also, if I include the aspectRatio(contentMode: .fit) the text is correct but the view now only takes up a part of the frame.
How can I prevent this from happening?

For a UIView, the draw(_ rect: CGRect) func is only called when UIKit decides it needs to be called.
If you put a print("inside draw()") statement in there, you'll see that it is not called when you rotate the device.
For UIKit implementation, we can implement layoutSubviews() add a call to setNeedsDisplay() to tell UIKit to call draw().
I don't use SwiftUI ... but quick try looks like this will work:
class UIViewTestClass: UIView {
override func draw(_ rect: CGRect) {
"Test".draw(at: CGPoint(x: rect.midX, y: rect.midY))
let context = UIGraphicsGetCurrentContext()
context?.addRect(rect)
context?.stroke(rect)
}
override func layoutSubviews() {
super.layoutSubviews()
setNeedsDisplay()
}
}

Related

UITapGestureRecognizer not triggered when used by 2 overlapping UIViewRepresentable

Because I need to know whether user taps with finger or pencil, I had to create a UIViewRepresentable with a UITapGestureRecognizer. Everything works fine when this View is used with other SwiftUI views. But when stack 2 of this view, and I clip the top one, the other one never catches any tap event.
Here is my custom view:
struct UIKitView: UIViewRepresentable {
let color: UIColor
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = color
let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(gesture:)))
view.addGestureRecognizer(tapGesture)
return view
}
func updateUIView(_ uiView: UIView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(color: color)
}
class Coordinator: NSObject {
let color: UIColor
init(color: UIColor) {
self.color = color
}
#objc func handleTap(gesture: UITapGestureRecognizer) {
print("Tapped in \(color == .yellow ? "yellow" : "red")")
}
}
}
And here how it is used in SwiftUI View:
ZStack {
UIKitView(color: .yellow)
UIKitView(color: .red)
.clipShape(Circle())) // not required, just visual
.contentShape(Circle()))
}
The tap is always catched by the red view.
Then I've overriden the hitTest() function of my UIVIewRepresentable to check if the tap is inside the clipîng shape. The tap is then detected in the red circle, but nothing tiggers anymore outside.
Any idea why?
You can override this method in CircleView:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let center = CGPoint(x: bounds.size.width/2, y: bounds.size.height/2)
return pow(center.x-point.x, 2) + pow(center.y - point.y, 2) <= pow(bounds.size.width/2, 2)
}
Source
FYI there is also a mistake in your UIViewRepresentable, you need this
func updateUIView(_ uiView: UIView, context: Context) {
view.backgroundColor = color
}

SwiftUI - Unbalanced Calls To Begin/End Appearance Transitions For

I have a .onTapGesture modifier that when tapped presents an ImagePicker before immediately dismissing it.
#State private var updateInfo = false
var body: some View {
HStack {
placeholder.image
.resizable()
.aspectRatio(contentMode: .fill)
.font(.system(size: 16, weight: .medium))
.frame(width: 100, height: 100)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.1), radius: 1, x: 0, y: 1)
.onTapGesture {
updateInfo.showImagePicker = true
}
.sheet(isPresented: $updateInfo.showImagePicker) {
ImagePicker(showImagePicker: $updateInfo.showImagePicker, pickedImage: $updateInfo.image, imageData: $updateInfo.imageData)
}
}
}
Here's my ImagePicker
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
#Binding var showImagePicker: Bool
#Binding var pickedImage: Image
#Binding var imageData: Data
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
return
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parentImagePicker: ImagePicker
init(_ imagePicker: ImagePicker) {
self.parentImagePicker = imagePicker
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
parentImagePicker.pickedImage = Image(uiImage: uiImage)
if let mediaImage = uiImage.jpegData(compressionQuality: 0.5) {
parentImagePicker.imageData = mediaImage
}
parentImagePicker.showImagePicker = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parentImagePicker.showImagePicker = false
}
}
}
I keep getting this error in the console: Unbalanced calls to begin/end appearance transitions for <_UIImagePickerPlaceholderViewController: 0x...>
Not sure where I'm transitioning incorrectly. I have a tab bar view with three tabs. The final tab has a navigation bar item that pushes the detail updateInfo view.
I tap the placeholder image and system presents the image picker controller (kind of) before immediately dismissing. I tap it again and the image picker controller presents.
Thoughts on why it dismisses the first time?
A solution that is not exactly a solution
I was trying to call the ImagePickerController on a view and kept getting the unbalanced views error. So I created a different view entirely for picking a new image. The error has not been addressed, but the issue for me is solved.

Transparent and Blurred fullScreenCover modal in SwiftUI?

I'm the fullScreenCover to show a modal in my SwiftUI project.
However, I need to show the modal with a transparent and blurred background.
But I can't seem to be able to achieve this at all.
This is my code:
.fullScreenCover(isPresented: $isPresented) {
VStack(spacing: 20) {
Spacer()
.frame(maxWidth: .infinity, minHeight: 100)
.background(Color.black)
.opacity(0.3)
//Text("modal")
}
.background(SecondView()) // << helper !!
}
And I have this on the same View:
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
The above code will open the fullscreen modal but its not transparent or blurred at all.
This is what I get:
is there something else I need to do?
Reference to where I got the above code from:
SwiftUI: Translucent background for fullScreenCover
I removed some extraneous stuff and made sure that there was content in the background that you could see peeking through.
Without the alpha that is suggested in a different answer, the effect is very subtle, but there.
struct ContentView : View {
#State var isPresented = false
var body: some View {
VStack {
Text("Some text")
Spacer()
Text("More")
Spacer()
Button(action: {
isPresented.toggle()
}) {
Text("Show fullscreenview")
}
.fullScreenCover(isPresented: $isPresented) {
Text("modal")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(BackgroundBlurView())
}
Spacer()
Text("More text")
}
}
}
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Note the extremely subtle blue you can see from the Button shining through. More obvious in dark mode.
The effect may get more dramatic as you add more color and content to the background view. Adding alpha certainly makes things more obvious, though.
#Asperi answer from here is totally correct. Just need to add alpha to blur view.
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
view.alpha = 0.5 //< --- here
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Another approach is to use ViewControllerHolder reference link
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
extension UIViewController {
func present<Content: View>(backgroundColor: UIColor = UIColor.clear, #ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.view.backgroundColor = backgroundColor
toPresent.modalPresentationStyle = .overCurrentContext
toPresent.modalTransitionStyle = .coverVertical
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
//Add blur efect
let blurEfect = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
blurEfect.frame = UIScreen.main.bounds
blurEfect.alpha = 0.5
toPresent.view.insertSubview(blurEfect, at: 0)
self.present(toPresent, animated: true, completion: nil)
}
}
struct ContentViewNeo: View {
#Environment(\.viewController) private var viewControllerHolder: UIViewController?
var body: some View {
VStack {
Text("Background screen")
Button("Open") {
viewControllerHolder?.present(builder: {
SheetView()
})
}
}
}
}
struct SheetView: View {
var body: some View {
Text("Sheet view")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}

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

Google AdMob integration in SwiftUI

I found nowhere an example how to integrate it with swiftui. Does anybody found a tutorial?
The problem is the part with the root controller.
in the Apple SwiftUI tutorial - integration in SwiftUI
you can find that how to solve this question with UIViewControllerRepresentable
and I create an example like this
import GoogleMobileAds
import SwiftUI
import UIKit
struct GADBannerViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let view = GADBannerView(adSize: kGADAdSizeBanner)
let viewController = UIViewController()
view.adUnitID = "your ad unit id in there."
view.rootViewController = viewController
viewController.view.addSubview(view)
viewController.view.frame = CGRect(origin: .zero, size: kGADAdSizeBanner.size)
view.load(GADRequest())
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
then you can using GADBannerViewController in your SwiftUI view's body like that
HStack {
Spacer()
GADBannerViewController()
.frame(width: kGADAdSizeBanner.size.width, height: kGADAdSizeBanner.size.height)
Spacer()
}
if you have any questions, please let me know.👌
To improve on Mcatach and avoid adding the view to the app's root view controller:
struct GADBannerViewController: UIViewControllerRepresentable {
#State private var banner: GADBannerView = GADBannerView(adSize: kGADAdSizeBanner)
func makeUIViewController(context: Context) -> UIViewController {
let bannerSize = GADBannerViewController.getAdBannerSize()
let viewController = UIViewController()
banner.adSize = bannerSize
banner.adUnitID = "ca-pub-ad-id-12345678"
banner.rootViewController = viewController
viewController.view.addSubview(banner)
viewController.view.frame = CGRect(origin: .zero, size: bannerSize.size)
banner.load(Ads.createRequest())
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context){
let bannerSize = GADBannerViewController.getAdBannerSize()
banner.frame = CGRect(origin: .zero, size: bannerSize.size)
banner.load(Ads.createRequest())
}
static func getAdBannerSize() -> GADAdSize {
if let rootView = UIApplication.shared.windows.first?.rootViewController?.view {
let frame = rootView.frame.inset(by: rootView.safeAreaInsets)
return GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(frame.width)
}
//No root VC, use 320x50 ad banner
return kGADAdSizeBanner
}
}
The layout code:
private func adSection() -> some View {
HStack {
let size = GADBannerViewController.getAdBannerSize()
Spacer()
GADBannerViewController()
.frame(width: size.size.width, height: size.size.height)
Spacer()
}
}
You should use UIViewRepresentable instead of UIViewControllerRepresentable.
I implemented the Adaptive banner with this code:
struct AdView : UIViewRepresentable {
#State private var banner: GADBannerView = GADBannerView(adSize: kGADAdSizeBanner)
func makeUIView(context: UIViewRepresentableContext<AdView>) -> GADBannerView {
#if DEBUG
banner.adUnitID = "ca-app-pub-debug"
#else
banner.adUnitID = "ca-app-pub-prod"
#endif
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
return banner
}
banner.rootViewController = rootViewController
let frame = { () -> CGRect in
return banner.rootViewController!.view.frame.inset(by: banner.rootViewController!.view.safeAreaInsets)
}()
let viewWidth = frame.size.width
banner.adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(viewWidth)
banner.load(GADRequest())
return banner
}
func updateUIView(_ uiView: GADBannerView, context: UIViewRepresentableContext<AdView>) {
}
}
Then you can call on your Stack using
AdView().frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 300)
.frame(width: kGADAdSizeBanner.size.width, height: kGADAdSizeBanner.size.height)