Admob BannerView in SwiftUI - swiftui

I am attempting to wrap the GADBannerView with a UIViewControllerRepresentable so that I can use it in my SwiftUI views but I encountered a situation where the background layer of the viewController covers the main list view. This resulted in a situation where the list is not scrollable.
I would like the bannerView to be anchored to the bottom of the view, but just above the tabView.
Code:
struct LaunchView: View {
var body: some View {
TabView {
HomeView()
.tabItem {
Label {
Text("Home")
} icon: {
Image(systemName: "gear")
}
}
}
}
}
struct HomeView: View {
var body: some View {
NavigationView {
ZStack {
List {
ForEach(0..<20, id: \.self) { _ in
Text("Hello, World!")
}
}
VStack {
Spacer()
JRBannerView()
}
}
.navigationTitle(Text("Home"))
}
}
}
class JRBannerViewController: UIViewController {
lazy var bannerView: GADBannerView = {
let v = GADBannerView()
v.translatesAutoresizingMaskIntoConstraints = false
v.adUnitID = TEST_AD_ID
v.rootViewController = self
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
loadBannerAd()
}
func setupViews() {
view.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.5) //<== obscures the main list view behind
view.addSubview(bannerView)
bannerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
bannerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
func loadBannerAd() {
let viewWidth = view.frame.inset(by: view.safeAreaInsets).size.width
bannerView.adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(viewWidth)
bannerView.load(GADRequest())
}
}
struct JRBannerView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> JRBannerViewController {
return JRBannerViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Results:
How do I compact the background view of the viewController to the same height of the bannerView so that the list is scrollable?

Related

SwiftUI's custom modal logic: how to present view from parent

Coming from UIKit, I'm building my own modal navigation logic in SwiftUI, because I want custom layouts and animations. Here, I want a generic bottom sheet like so:
I have achieved something close with the following code:
enum ModalType: Equatable {
case normal // ...
#ViewBuilder
var screen: some View {
switch self {
case .normal: ModalView()
// ...
}
}
}
struct ContentView: View {
#State var presentedModal: ModalType?
var body: some View {
VStack {
Button("Present modal", action: { presentedModal = .normal }).foregroundColor(.black)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
.modifier(ModalBottomViewModifier(item: $presentedModal) { $0.screen })
}
}
struct ModalView: View {
#Environment(\.dismissModal) private var dismissModal
var body: some View {
VStack {
Button("Close", action: { dismissModal() })
}
.frame(maxWidth: .infinity)
.frame(height: 300)
.background(
RoundedRectangle(cornerRadius: 32)
.fill(.black.opacity(0.5))
.edgesIgnoringSafeArea([.bottom])
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// MARK: - Modal logic
struct ModalBottomViewModifier<Item:Equatable, V:View>: ViewModifier {
#Binding var item: Item?
#ViewBuilder var view: (Item) -> V
func body(content: Content) -> some View {
ZStack(alignment: .bottom) {
content
if let item = item {
view(item)
.environment(\.dismissModal, { self.item = nil })
.transition(.move(edge: .bottom))
}
}
.animation(.easeOut, value: item)
}
}
private struct ModalDismissKey: EnvironmentKey {
static let defaultValue: () -> Void = {}
}
extension EnvironmentValues {
var dismissModal: () -> Void {
get { self[ModalDismissKey.self] }
set { self[ModalDismissKey.self] = newValue }
}
}
Now I'd like to make this system reusable, so that I don't have to add the ModalBottomViewModifier to all my app screens. For that, I'd like to be able to apply the modifier to the button instead of the screen, just like it's possible with fullScreenCover:
Button("Present modal", action: { isPresented = true }).foregroundColor(.black)
.fullScreenCover(isPresented: $isPresented) { ModalView() }
This is not possible with my current solution, because the modal view will appear next to the button and not fullscreen.
How can I achieve this? Or should I be doing something different?
Here's a simple solution using UIKit:
extension View {
func presentModalView<Content: View, Item: Equatable>(item: Binding<Item?>, #ViewBuilder view: #escaping (Item) -> Content) -> some View {
func present() {
guard let itemy = item.wrappedValue else {return}
DispatchQueue.main.async {
let topMostController = self.topMostController()
let someView = VStack {
Spacer()
view(itemy)
.environment(\.dismissModal, {item.wrappedValue = nil})
}
let viewController = UIHostingController(rootView: someView)
viewController.view?.backgroundColor = .clear
viewController.modalPresentationStyle = .overFullScreen
topMostController.present(viewController, animated: true)
}
}
return self.onChange(of: item.wrappedValue) { value in
if value != nil {
present()
}else {
topMostController().dismiss(animated: true)
}
}.onAppear {
if item.wrappedValue != nil {
present()
}
}
}
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.windows.first!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
}
Usage:
struct ContentView: View {
#State var presentedModal: ModalType?
var body: some View {
VStack {
Button("Present modal", action: { presentedModal = .normal }).foregroundColor(.black)
.presentModalView(item: $presentedModal, view: {$0.screen})
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
}
}

How we can adding a search bar with side bar icon to the navigation view?

I want to add a search bar to the navigation bar, but I do not know how to use search bar with sidebar icon in the same HStack. I put example screenshot with ContentView code. Any help would be appreciated.
Screenshot:
ContentView:
struct ContentView: View {
#State private var isShowing = false
var body: some View {
ZStack {
if isShowing {
SideMenuView(isShowing: $isShowing)
}
TabView {
NavigationView {
HomeView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "1.circle")
Text("Page 1")
}
NavigationView {
HomeTwoView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "2.circle")
Text("Page 2")
}
}
.edgesIgnoringSafeArea(.bottom)
//.cornerRadius(isShowing ? 20 : 0) //<< disabled due to strange effect
.offset(x: isShowing ? 300 : 0, y: isShowing ? 44: 0)
.scaleEffect(isShowing ? 0.8 : 1)
}.onAppear {
isShowing=false
}
}
}
As I mentioned in comments this is not possible in SwiftUI (2.0) yet. What you can do is integrating with UIKit.
Integrate with UIKit
class UIKitSearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.definesPresentationContext = true
self.searchController.searchResultsUpdater = self
}
}
extension UIKitSearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: UIKitSearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: UIKitSearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
Usage
struct Example: View {
#StateObject var searchBar = UIKitSearchBar()
var body: some View {
NavigationView {
Text("Example")
.add(searchBar)
.navigationTitle("Example")
}
}
}
In my own project I am using computed property to filter stuff, it can be helpful for you too. Here is my code:
var filteredExams: [Exam] {
examModel.exams.filter({ searchBar.text.isEmpty || $0.examName.localizedStandardContains(searchBar.text)})
}
Screenshot

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

How to change navigationBar background color locally

I tried this method but it is global which is undesired.
struct ExperienceView: View {
init() {
UINavigationBar.appearance().barTintColor = #colorLiteral(red: 0.1764705882, green: 0.2196078431, blue: 0.3098039216, alpha: 1)
UINavigationBar.appearance().isTranslucent = false
UINavigationBar.appearance().shadowImage = UIImage()
}
}
And I tried this method but it is not working, I don't know why. I even tried copying original code on SwiftUI update navigation bar title color, still not working.
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
Text("Don't use .appearance()!")
}
.navigationBarTitle("Try it!", displayMode: .inline)
.background(UINavigationConfiguration { nc in
nc.navigationBar.barTintColor = .blue
nc.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.white]
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct UINavigationConfiguration: UIViewControllerRepresentable {
var config: (UINavigationController) -> Void = {_ in }
typealias UIViewControllerType = UINavigationController
func makeUIViewController(context: Context) -> UINavigationController {
return UINavigationController()
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
if let nc = uiViewController.navigationController {
self.config(nc)
}
}
}
In the place you try to get navigation controller it is not injected yet. Here is fixed configurator (tested with Xcode 12.1 / iOS 14.1):
struct UINavigationConfiguration: UIViewControllerRepresentable {
var config: (UINavigationController) -> Void = {_ in }
func makeUIViewController(context: Context) -> UIViewController {
let controller = UIViewController()
DispatchQueue.main.async {
if let nc = controller.navigationController {
self.config(nc)
}
}
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}

SwiftUI chat app: the woes of reversed List and Context Menu

I am building a chat app in SwiftUI. To show messages in a chat, I need a reversed list (the one that shows most recent entries at the bottom and auto-scrolls to the bottom). I made a reversed list by flipping both the list and each of its entries (the standard way of doing it).
Now I want to add Context Menu to the messages. But after the long press, the menu shows messages flipped. Which I suppose makes sense since it plucks a flipped message out of the list.
Any thoughts on how to get this to work?
import SwiftUI
struct TestView: View {
var arr = ["1aaaaa","2bbbbb", "3ccccc", "4aaaaa","5bbbbb", "6ccccc", "7aaaaa","8bbbbb", "9ccccc", "10aaaaa","11bbbbb", "12ccccc"]
var body: some View {
List {
ForEach(arr.reversed(), id: \.self) { item in
VStack {
Text(item)
.height(100)
.scaleEffect(x: 1, y: -1, anchor: .center)
}
.contextMenu {
Button(action: { }) {
Text("Reply")
}
}
}
}
.scaleEffect(x: 1, y: -1, anchor: .center)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
The issue with flipping is that you need to flip the context menu and SwiftUI does not give this much control.
The better way to handle this is to get access to embedded UITableView(on which you will have more control) and you need not add additional hacks.
Here is the demo code:
import SwiftUI
import UIKit
struct TestView: View {
#State var arr = ["1aaaaa","2bbbbb", "3ccccc", "4aaaaa","5bbbbb", "6ccccc", "7aaaaa","8bbbbb", "9ccccc", "10aaaaa","11bbbbb", "12ccccc"]
#State var tableView: UITableView? {
didSet {
self.tableView?.adaptToChatView()
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.tableView?.scrollToBottom(animated: true)
}
}
}
var body: some View {
NavigationView {
List {
UIKitView { (tableView) in
DispatchQueue.main.async {
self.tableView = tableView
}
}
ForEach(arr, id: \.self) { item in
Text(item).contextMenu {
Button(action: {
// change country setting
}) {
Text("Choose Country")
Image(systemName: "globe")
}
Button(action: {
// enable geolocation
}) {
Text("Detect Location")
Image(systemName: "location.circle")
}
}
}
}
.navigationBarTitle(Text("Chat View"), displayMode: .inline)
.navigationBarItems(trailing:
Button("add chat") {
self.arr.append("new Message: \(self.arr.count)")
self.tableView?.adaptToChatView()
DispatchQueue.main.async {
self.tableView?.scrollToBottom(animated: true)
}
})
}
}
}
extension UITableView {
func adaptToChatView() {
let offset = self.contentSize.height - self.visibleSize.height
if offset < self.contentOffset.y {
self.tableHeaderView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: self.contentSize.width, height: self.contentOffset.y - offset))
}
}
}
extension UIScrollView {
func scrollToBottom(animated:Bool) {
let offset = self.contentSize.height - self.visibleSize.height
if offset > self.contentOffset.y {
self.setContentOffset(CGPoint(x: 0, y: offset), animated: animated)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
final class UIKitView : UIViewRepresentable {
let callback: (UITableView) -> Void //return TableView in CallBack
init(leafViewCB: #escaping ((UITableView) -> Void)) {
callback = leafViewCB
}
func makeUIView(context: Context) -> UIView {
let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
y: CGFloat.leastNormalMagnitude,
width: CGFloat.leastNormalMagnitude,
height: CGFloat.leastNormalMagnitude))
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let tableView = uiView.next(UITableView.self) {
callback(tableView) //return tableview if find
}
}
}
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
You can create a custom modal for reply and show it with long press on every element of the list without showing contextMenu.
#State var showYourCustomReplyModal = false
#GestureState var isDetectingLongPress = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.5)
.updating($isDetectingLongPress) { currentstate, gestureState,
transaction in
gestureState = currentstate
}
.onEnded { finished in
self.showYourCustomReplyModal = true
}
}
Apply it like:
ForEach(arr, id: \.self) { item in
VStack {
Text(item)
.height(100)
.scaleEffect(x: 1, y: -1, anchor: .center)
}.gesture(self.longPress)
}
As of iOS 14, SwiftUI has ScrollViewReader which can be used to position the scrolling. GeometryReader along with minHeight and Spacer() can make a VStack that uses the full screen while displaying messages starting at the bottom. Items are read from and appended to an array in the usual first-in first-out order.
SwiftUI example:
struct ContentView: View {
#State var items: [Item] = []
#State var text: String = ""
#State var targetItem: Item?
var body: some View {
VStack {
ScrollViewReader { scrollView in
ChatStyleScrollView() {
ForEach(items) { item in
ItemView(item: item)
.id(item.id)
}
}
.onChange(of: targetItem) { item in
if let item = item {
withAnimation(.default) {
scrollView.scrollTo(item.id)
}
}
}
TextEntryView(items: $items, text: $text, targetItem: $targetItem)
}
}
}
}
//MARK: - Item Model with unique identifier
struct Item: Codable, Hashable, Identifiable {
var id: UUID
var text: String
}
//MARK: - ScrollView that pushes text to the bottom of the display
struct ChatStyleScrollView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
GeometryReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
VStack {
Spacer()
content
}
.frame(minHeight: proxy.size.height)
}
}
}
}
//MARK: - A single item and its layout
struct ItemView: View {
var item: Item
var body: some View {
HStack {
Text(item.text)
.frame(height: 100)
.contextMenu {
Button(action: { }) {
Text("Reply")
}
}
Spacer()
}
}
}
//MARK: - TextField and Send button used to input new items
struct TextEntryView: View {
#Binding var items: [Item]
#Binding var text: String
#Binding var targetItem: Item?
var body: some View {
HStack {
TextField("Item", text: $text)
.frame(height: 44)
Button(action: send) { Text("Send") }
}
.padding(.horizontal)
}
func send() {
guard !text.isEmpty else { return }
let item = Item(id: UUID(), text: text)
items.append(item)
text = ""
targetItem = item
}
}
If someone is searching for a solution in UIKit: instead of the cell, you should use the contentView or a subview of the contentView as a paramterer for the UITargetedPreview. Like this:
extension CustomScreen: UITableViewDelegate {
func tableView(_ tableView: UITableView,
contextMenuConfigurationForRowAt indexPath: IndexPath,
point: CGPoint) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: indexPath as NSCopying,
previewProvider: nil) { _ in
// ...
return UIMenu(title: "", children: [/* actions */])
}
}
func tableView(
_ tableView: UITableView,
previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
getTargetedPreview(for: configuration.identifier as? IndexPath)
}
func tableView(
_ tableView: UITableView,
previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
getTargetedPreview(for: configuration.identifier as? IndexPath)
}
}
extension CustomScreen {
private func getTargetedPreview(for indexPath: IndexPath?) -> UITargetedPreview? {
guard let indexPath = indexPath,
let cell = tableView.cellForRow(at: indexPath) as? CustomTableViewCell else { return nil }
return UITargetedPreview(view: cell.contentView,
parameters: UIPreviewParameters().then { $0.backgroundColor = .clear })
}
}
If I understood it correctly, why don't you order your array in the for each loop or prior. Then you do not have to use any scaleEffect at all. Later if you get your message object, you probably have a Date assinged to it, so you can order it by the date. In your case above you could use:
ForEach(arr.reverse(), id: \.self) { item in
...
}
Which will print 12ccccc as first message at the top, and 1aaaaa as last message.