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)
Related
I implemented a UIScrollView from UIKit in SwiftUI (without storyboard) because there is nothing similar in SwiftUI. It works quite well so far, but the ScrollView is not limited by the size of the content, but you can still scroll over the edge of the content. The stronger the zoom factor, the stronger this effect becomes apparent.
UIScrollView Wrapper in SwiftUI:
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
#Binding var didZoom: Bool
private var content: Content
init(didZoom: Binding<Bool>, #ViewBuilder content: () -> Content) {
_didZoom = didZoom
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content), didZoom: $didZoom)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
#Binding var didZoom: Bool
init(hostingController: UIHostingController<Content>, didZoom: Binding<Bool>) {
self.hostingController = hostingController
_didZoom = didZoom
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
didZoom = !(scrollView.zoomScale == scrollView.minimumZoomScale)
}
}
}
SwiftUI ContentView:
struct ContentView: View {
var body: some View {
ZoomableScrollView {
Rectangle()
.frame(width: 420, height: 600)
}
}
}
You're making whole screen zoomable if you want to make only rectangle zoomable then only wrap that rectangle within zoomablescrollview
struct ContentView: View {
var body: some View {
ZStack{
ZoomableScrollView {
Rectangle()
.frame(width: 420, height: 600)
}.frame(width: 420,height: 600)
}
}
}
i have a ScrollView from UIKit and use it for SwiftUI: Is there any way to make a paged ScrollView in SwiftUI?
Question: How can I scroll in the UIScrollView to a position with a button click on a button in a SwiftUI View OR what is also good for my needs to scroll to a position when first displaying the ScrollView
I tried contentOffset but this didnt work. Perhaps I've done something wrong.
ScrollViewWrapper:
class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = false
v.alwaysBounceVertical = true
return v
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
SwiftUI usage:
struct ContentView: View{
#ObservedObject var search = SearchBar()
var body: some View{
NavigationView{
GeometryReader{geo in
UIScrollViewWrapper{ //<-----------------
VStack{
ForEach(0..<10){i in
Text("lskdfj")
}
}
.frame(width: geo.size.width)
}
.navigationBarTitle("Test")
}
}
}
}
We will first declare the offset property in the UIViewControllerRepresentable, with the propertyWrapper #Binding, because its value can be changed by the scrollview or by the parent view (the ContentView).
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
#Binding var offset: CGPoint
init(offset: Binding<CGPoint>, #ViewBuilder content: #escaping () -> Content) {
self.content = content
_offset = offset
}
// ....//
}
If the offset changes cause of the parent view, we must apply these changes to the scrollView in the updateUIViewController function (which is called when the state of the view changes) :
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(content())
viewController.scrollView.contentOffset = offset
}
When the offset changes because the user scrolls, we must reflect this change on our Binding. To do this we must declare a Coordinator, which will be a UIScrollViewDelegate, and modify the offset in its scrollViewDidScroll function :
class Controller: NSObject, UIScrollViewDelegate {
var parent: UIScrollViewWrapper<Content>
init(parent: UIScrollViewWrapper<Content>) {
self.parent = parent
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
parent.offset = scrollView.contentOffset
}
}
and, in struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable
func makeCoordinator() -> Controller {
return Controller(parent: self)
}
Finally, for the initial offset (this is important otherwise your starting offset will always be 0), this happens in the makeUIViewController:
you have to add these lines:
vc.view.layoutIfNeeded ()
vc.scrollView.contentOffset = offset
The final project :
import SwiftUI
struct ContentView: View {
#State private var offset: CGPoint = CGPoint(x: 0, y: 200)
let texts: [String] = (1...100).map {_ in String.random(length: Int.random(in: 6...20))}
var body: some View {
ZStack(alignment: .top) {
GeometryReader { geo in
UIScrollViewWrapper(offset: $offset) { //
VStack {
Text("Start")
.foregroundColor(.red)
ForEach(texts, id: \.self) { text in
Text(text)
}
}
.padding(.top, 40)
.frame(width: geo.size.width)
}
.navigationBarTitle("Test")
}
HStack {
Text(offset.debugDescription)
Button("add") {
offset.y += 100
}
}
.padding(.bottom, 10)
.frame(maxWidth: .infinity)
.background(Color.white)
}
}
}
class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = false
v.alwaysBounceVertical = true
return v
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
pinEdges(of: scrollView, to: view)
hostingController.willMove(toParent: self)
scrollView.addSubview(hostingController.view)
pinEdges(of: hostingController.view, to: scrollView)
hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
#Binding var offset: CGPoint
init(offset: Binding<CGPoint>, #ViewBuilder content: #escaping () -> Content) {
self.content = content
_offset = offset
}
func makeCoordinator() -> Controller {
return Controller(parent: self)
}
func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.scrollView.contentInsetAdjustmentBehavior = .never
vc.hostingController.rootView = AnyView(content())
vc.view.layoutIfNeeded()
vc.scrollView.contentOffset = offset
vc.scrollView.delegate = context.coordinator
return vc
}
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(content())
viewController.scrollView.contentOffset = offset
}
class Controller: NSObject, UIScrollViewDelegate {
var parent: UIScrollViewWrapper<Content>
init(parent: UIScrollViewWrapper<Content>) {
self.parent = parent
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
parent.offset = scrollView.contentOffset
}
}
}
You will need to pass a #Binding var offset: CGPoint into the UIScrollViewWrapper then when the button is clicked in your SwiftUI view, you can update the binding value which can then be used in the update method for UIViewControllerRepresentable. Another idea is to use UIViewRepresentable instead and use that with UIScrollView. Here is a helpful article doing that and setting its offset: https://www.fivestars.blog/articles/scrollview-offset/.
Using Swift5.3.2, iOS14.4.1, Xcode12.4,
I am trying to use the .simultaneousGesture modifier in SwiftUI.
As far as I understood, this modifier should make sure that gestures (such as tap, longpress, magnification etc) should be able to co-exist within a View.
In my example I am using a ZoomableScrollView. And it works fine as long as I do not use the simultaneousGesture.
But as soon as I use the extra simultaneousGesture, the ZoomableScrollView is no longer "zoomable" (i.e. none of its gestures work anymore).
What can I do to make the zoom still work AND get an extra dragGesture ?
import SwiftUI
struct MediaTabView: View {
#GestureState private var dragOffset: CGFloat = -100
var body: some View {
ZoomableScrollView {
Image(uiImage: UIImage(contentsOfFile: url.path)!)
.resizable()
.scaledToFit()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.simultaneousGesture(
DragGesture()
.updating($dragOffset) { (value, gestureState, transaction) in
let delta = value.location.x - value.startLocation.x
if delta > 10 { // << some appropriate horizontal threshold here
gestureState = delta
print(delta)
}
}
.onEnded {
if $0.translation.width > 100 {
// Go to the previous slide
print("on ended")
}
}
)
}
}
The code for the ZoomableScrollView is here :
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
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
hostedView.backgroundColor = .black
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
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.
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")
}
}