Google Ads 8.0 Interstitial SwiftUI - swiftui

Seems like there isn't many examples of using Google MobileAdsSDK 8.0 (iOS) with SwiftUI.
So far I have a class Interstitial
import GoogleMobileAds
import UIKit
final class Interstitial:NSObject, GADFullScreenContentDelegate{
var interstitial:GADInterstitialAd!
override init() {
super.init()
LoadInterstitial()
}
func LoadInterstitial(){
let req = GADRequest()
GADInterstitialAd.load(withAdUnitID: "...", request: req) { ad, error in
self.interstitial = ad
self.interstitial.fullScreenContentDelegate = self
}
}
func showAd(){
if self.interstitial != nil {
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
LoadInterstitial()
}
}
In my SwiftUI view i create a local variable Interstitial, and when an action is performed I call the showAd() function however when the ad displays it stops the code immediately following the showAd() call from running. So I think I need to somehow call showAd() and once the ad is dismissed then perform the remainder of my code in the view. As you can see above the Interstitial class is the delegate, but how do I "alert" my SwiftUI view that the ad was dismissed so I can execute the rest of the code? Below is my View.
import SwiftUI
struct MyView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var interstitial : Interstitial = Interstitial()
var body: some View {
VStack{
//... Display content
}
.navigationBarItems(trailing:
HStack{
Button(action: actionSheet) {
Image(systemName: "square.and.arrow.up")
}
}
)
}
func showAd(){
interstitial.showAd()
}
func actionSheet() {
showAd()
let data = createPDF()
let temporaryFolder = FileManager.default.temporaryDirectory
let fileName = "export.pdf"
let temporaryFileURL = temporaryFolder.appendingPathComponent(fileName)
do {
try data.write(to: temporaryFileURL)
let av = UIActivityViewController(activityItems: [try URL(resolvingAliasFileAt: temporaryFileURL)], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
} catch {
print(error)
}
}
}

By adding an excaping closure, you pass a function and perform the needed actions.
final class InterstitialAd: NSObject, GADFullScreenContentDelegate {
var completion: () -> Void
var interstitial: GADInterstitialAd!
init(completion: #escaping () -> Void) {
self.completion = completion
super.init()
LoadInterstitialAd()
}
func LoadInterstitialAd() {
let req = GADRequest()
GADInterstitialAd.load(withAdUnitID: Constants.AdmobIDs.saveImageBlockID, request: req) { ad, error in
self.interstitial = ad
self.interstitial.fullScreenContentDelegate = self
}
}
func show() {
if self.interstitial != nil {
let root = UIApplication.shared.windows.first?.rootViewController
self.interstitial.present(fromRootViewController: root!)
}
}
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
LoadInterstitialAd()
completion()
}
}

Related

UISearchController how to activate from SwiftUI

I found an integration of the uiSearchController in SwiftUI, but I don't know how to let it become active?
I found this:
I want that the searchBar becomes active when changing an Bool in the SwiftUI View with a #State for example.
If I add a Binding to the view modifier and set the isActive property of the searchController in
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
then is doesn't become active.
Im not really familiar with UIKit, perhaps anybody knows how to correctly activate the searchbar that one can start typing for a search.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
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)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
To activate a UISearchBar (which is what you're using), just do:
searchController.searchBar.becomeFirstResponder()
(from this answer)
Now all we need to do is reference searchController.searchBar from the SwiftUI view. First, add a function to your SearchBar class.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
/// add this function
func activate() {
searchController.searchBar.becomeFirstResponder()
}
}
Then, just call it. I think this is better than setting a #State, but if you require that, let me know and I'll edit my answer.
struct ContentView: View {
#StateObject var searchBar = SearchBar()
var body: some View {
NavigationView {
Button(action: {
searchBar.activate() /// activate the search bar
}) {
Text("Activate search bar")
}
.modifier(SearchBarModifier(searchBar: searchBar))
.navigationTitle("Navigation View")
}
}
}
Result:

MFMessageComposeViewController + SwiftUI Buggy Behavior

I'm using ViewControllerRepresentable to present a MFMessageComposeViewController so users can send texts from my app.
However, every time the view is presented, it's very buggy - elements randomly disappear, scrolling is off, and the screen flickers. Tested on iOS 14.2 and 14.3.
Here's the code:
import SwiftUI
import MessageUI
struct MessageView: UIViewControllerRepresentable {
var recipient: String
class Coordinator: NSObject, MFMessageComposeViewControllerDelegate {
var completion: () -> Void
init(completion: #escaping ()->Void) {
self.completion = completion
}
// delegate method
func messageComposeViewController(_ controller: MFMessageComposeViewController,
didFinishWith result: MessageComposeResult) {
controller.dismiss(animated: true, completion: nil)
completion()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator() {} // not using completion handler
}
func makeUIViewController(context: Context) -> MFMessageComposeViewController {
let vc = MFMessageComposeViewController()
vc.recipients = [recipient]
vc.messageComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: MFMessageComposeViewController, context: Context) {}
typealias UIViewControllerType = MFMessageComposeViewController
}
and my view
struct ContentView: View {
#State private var isShowingMessages = false
#State var result: Result<MFMailComposeResult, Error>? = nil
var body: some View {
VStack {
Button("Show Messages") {
self.isShowingMessages = true
}
.sheet(isPresented: self.$isShowingMessages) {
MessageView(recipient: "+15555555555")
}
.edgesIgnoringSafeArea(.bottom)
}
}
}
Is there something wrong with the way I'm presenting this view? Has anyone else experienced this behavior? Similar behavior happens with MFMailComposeViewController, but it's not as buggy.
5 minutes later, I realized I needed to add this when presenting the sheet:
MessageView(recipient: "+15555555555")
.ignoresSafeArea()
The view looked buggy because it was trying to account for the keyboard safe area and had a hard time doing it.

SKStoreProductViewController must be used in a modal view controller SWIFTUI

I am building a info page for my SwiftUI app. One item should open App Store, another mail. I have written UIViewControllerRepresentable for each.
MailView works fine totally. StoreView displays fine, but when pressed on Cancel button, throws exception
"*** Terminating app due to uncaught exception 'SKUnsupportedPresentationException', reason: 'SKStoreProductViewController must be used in a modal view controller'".
MailView goes fine into didFinish delegate method but StoreView does not go into didFinish delegate method, it crashes before going into this didFinish method. What am I doing wrong please?
import SwiftUI
import StoreKit
import MessageUI
struct InfoMoreAppsView: View {
#State var showAppAtStore = false
#State var reportBug = false
#State var result: Result<MFMailComposeResult, Error>? = nil
let otherAppName = "TheoryTest"
var body: some View {
VStack(alignment: .leading){
HStack{
Image(Helper.getOtherAppImageName(otherAppName: otherAppName))
Button(action: { self.showAppAtStore = true }) {
Text(otherAppName)
}
.sheet(isPresented: $showAppAtStore){
StoreView(appID: Helper.getOtherAppID(otherAppName: otherAppName))
}
}
Button(action: { self.reportBug = true }) {
Text("Report a bug")
}
.sheet(isPresented: $reportBug){
MailView(result: self.$result)
}
}
.padding()
.font(.title2)
}
}
struct StoreView: UIViewControllerRepresentable {
let appID: String
#Environment(\.presentationMode) var presentation
class Coordinator: NSObject, SKStoreProductViewControllerDelegate {
#Binding var presentation: PresentationMode
init(presentation: Binding<PresentationMode> ) {
_presentation = presentation
}
private func productViewControllerDidFinish(viewController: SKStoreProductViewController) {
$presentation.wrappedValue.dismiss()
viewController.dismiss(animated: true, completion: nil)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> SKStoreProductViewController {
let skStoreProductViewController = SKStoreProductViewController()
skStoreProductViewController.delegate = context.coordinator
let parameters = [ SKStoreProductParameterITunesItemIdentifier : appID]
skStoreProductViewController.loadProduct(withParameters: parameters)
return skStoreProductViewController
}
func updateUIViewController(_ uiViewController: SKStoreProductViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
}
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = context.coordinator
mailComposeViewController.setToRecipients([Constants.SUPPORT_EMAIL])
mailComposeViewController.setMessageBody(systemInfo(), isHTML: true)
return mailComposeViewController
}
func systemInfo() -> String {
let device = UIDevice.current
let systemVersion = device.systemVersion
let model = UIDevice.hardwareModel
let mailBody = "Model: " + model + ". OS: " + systemVersion
return mailBody
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
This isn't very "Swifty" or pretty but I got this to work without crashing by not wrapping the SKStoreProductViewController in a representable.
struct MovieView: View {
var vc:SKStoreProductViewController = SKStoreProductViewController()
var body: some View {
HStack(){
Button(action: {
let params = [
SKStoreProductParameterITunesItemIdentifier:"1179624268",
SKStoreProductParameterAffiliateToken:"11l4Cu",
SKStoreProductParameterCampaignToken:"hype_movie"
] as [String : Any]
// vc!.delegate = self
vc.loadProduct(withParameters: params, completionBlock: { (success,error) -> Void in
UIApplication.shared.windows.first?.rootViewController?.present(vc, animated: true, completion: nil)
})
}) {
HStack {
Image(systemName: "play.fill")
.font(.headline)
}
.padding(EdgeInsets(top: 6, leading:36, bottom: 6, trailing: 36))
.foregroundColor(.white)
.background(Color(red: 29/255, green: 231/255, blue: 130/255))
.cornerRadius(10)
}
Spacer()
}}
Since I was stuck on the same thing. Here is a quick solution I found working.
import StoreKit
import SwiftUI
import UIKit
struct StoreView: UIViewControllerRepresentable {
var dismissHandler: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> StoreViewController {
return StoreViewController(coordinator: context.coordinator)
}
func updateUIViewController(_ uiViewController: StoreViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
public func makeCoordinator() -> StoreViewCoordinator {
.init(dismissHandler: dismissHandler)
}
}
class StoreViewController: UIViewController {
let coordinator: StoreViewCoordinator
var storeController: SKStoreProductViewController?
init(coordinator: StoreViewCoordinator) {
self.coordinator = coordinator
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
storeController = SKStoreProductViewController()
storeController?.delegate = coordinator
storeController?.loadProduct(
withParameters: [SKStoreProductParameterITunesItemIdentifier: ******]
)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let storeController = storeController else {
return
}
present(storeController, animated: true)
}
}
class StoreViewCoordinator: NSObject, SKStoreProductViewControllerDelegate {
private let dismissHandler: () -> Void
init(dismissHandler: #escaping () -> Void) {
self.dismissHandler = dismissHandler
}
func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
dismissHandler()
}
}
and then I am using it inside ZStack like:
StoreView(
dismissHandler: { viewStore.send(.setShowingStore(false)) }
)
.isHidden(!viewStore.isShowingStore, remove: true)
I am using TCA, so setting a property will be different in your case

SwiftUI - Location in API call with CLLocationManager and CLGeocoder

I'm struggling with this for a long time without finding where I'm wrong (I know I'm wrong).
I have one API call with the location of the phone (this one is working), but I want the same API call with a manual location entered by a textfield (using Geocoding for retrieving Lat/Long). The geocoding part is ok and updated but not passed in the API call.
I also want this API call to be triggered when the TextField is cleared by the dedicated button back with the phone location.
Please, what am I missing? Thanks for your help.
UPDATE: This works on Xcode 12.2 beta 2 and should work on Xcode 12.0.1
This is the code:
My Model
import Foundation
struct MyModel: Codable {
let value: Double
}
My ViewModel
import Foundation
import SwiftUI
import Combine
final class MyViewModel: ObservableObject {
#Published var state = State.ready
#Published var value: MyModel = MyModel(value: 0.0)
#Published var manualLocation: String {
didSet {
UserDefaults.standard.set(manualLocation, forKey: "manualLocation")
}
}
#EnvironmentObject var coordinates: Coordinates
init() {
manualLocation = UserDefaults.standard.string(forKey: "manualLocation") ?? ""
}
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
private var url: URL {
get {
return URL(string: "https://myapi.com&lat=\(coordinates.latitude)&lon=\(coordinates.longitude)")!
}
}
let urlSession = URLSession.shared
var dataTask: AnyPublisher<MyModel, Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load(){
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("⚠️ API Call finished")
break
case let .failure(error):
print("❌ API Call failure")
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.value = value
print("👍 API Call loaded")
}
))
}
}
The Location Manager
import Foundation
import SwiftUI
import Combine
import CoreLocation
import MapKit
final class Coordinates: NSObject, ObservableObject {
#EnvironmentObject var myViewModel: MyViewModel
#Published var latitude: Double = 0.0
#Published var longitude: Double = 0.0
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
deinit {
locationManager.stopUpdatingLocation()
}
}
extension Coordinates: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
latitude = location.coordinate.latitude
longitude = location.coordinate.longitude
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
self.placemark = places?[0]
})
self.locationManager.stopUpdatingLocation()
}
}
extension Coordinates {
func getLocation(from address: String, completion: #escaping (_ location: CLLocationCoordinate2D?)-> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location?.coordinate else {
completion(nil)
return
}
completion(location)
}
}
}
The View
import Foundation
import SwiftUI
struct MyView: View {
#EnvironmentObject var myViewModel: MyViewModel
#EnvironmentObject var coordinates: Coordinates
private var icon: Image { return Image(systemName: "location.fill") }
var body: some View {
VStack{
VStack{
Text("\(icon) \(coordinates.placemark?.locality ?? "Unknown location")")
Text("Latitude: \(coordinates.latitude)")
Text("Longitude: \(coordinates.longitude)")
}
VStack{
Text("UV Index: \(myViewModel.value.value)")
.disableAutocorrection(true)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
HStack{
TextField("Manual location", text: $myViewModel.manualLocation)
if !myViewModel.manualLocation.isEmpty{
Button(action: { clear() }) { Image(systemName: "xmark.circle.fill").foregroundColor(.gray) }
}
}
}.padding()
}
func commit() {
coordinates.getLocation(from: self.myViewModel.manualLocation) { places in
coordinates.latitude = places?.latitude ?? 0.0
coordinates.longitude = places?.longitude ?? 0.0
}
myViewModel.load()
}
func clear() {
myViewModel.manualLocation = ""
myViewModel.load()
}
}

Send Message to Apple Watch using WCSession in SwiftUI

I did a exmaple long time ago how to send a simple message from an iPhone to a Apple Watch using Swift:
import UIKit
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate {
// MARK: Outlets
#IBOutlet weak var textField: UITextField!
// MARK: Variables
var wcSession : WCSession! = nil
// MARK: Overrides
override func viewDidLoad() {
super.viewDidLoad()
wcSession = WCSession.default
wcSession.delegate = self
wcSession.activate()
}
// MARK: Button Actions
#IBAction func sendText(_ sender: Any) {
let txt = textField.text!
let message = ["message":txt]
wcSession.sendMessage(message, replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
// MARK: WCSession Methods
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// Code
}
func sessionDidBecomeInactive(_ session: WCSession) {
// Code
}
func sessionDidDeactivate(_ session: WCSession) {
// Code
}
}
Now I'm trying to do the same using SwiftUI but no success so far.
Can anyone help with this problem?
I just need to know how to use the WCSession Class and the WCSessionDelegate with SwiftUI.
Thanks
I just had the same question as you and I figured it out:
First you need to implement a class that conforms to WCSessionDelegate. I like to use a separate class for that:
import WatchConnectivity
class ConnectivityProvider: NSObject, WCSessionDelegate {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
}
func send(message: [String:Any]) -> Void {
session.sendMessage(message, replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// code
}
func sessionDidBecomeInactive(_ session: WCSession) {
// code
}
func sessionDidDeactivate(_ session: WCSession) {
// code
}
}
Now you need a ViewModel that takes your ConnectivityProvider as an argument. The ViewModel will be responsible for the connection of your View and the ConnectivityProvider. It also holds the value for the Textfield that later gets defined inside your View.
import SwiftUI
final class ViewModel: ObservableObject {
private(set) var connectivityProvider: ConnectivityProvider
var textFieldValue: String = ""
init(connectivityProvider: ConnectivityProvider) {
self.connectivityProvider = connectivityProvider
}
func sendMessage() -> Void {
let txt = textFieldValue
let message = ["message":txt]
connectivityProvider.send(message: message)
}
}
Now you can build a simple View that consists of a Textfield and a Button. Your View will be dependent on your ViewModel that you just defined.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
TextField("Message Content", text: $viewModel.textFieldValue)
Button(action: {
self.viewModel.sendMessage()
}) {
Text("Send Message")
}
}
}
}
Last but not least you need to combine your ConnectivityProvider, ViewModel and View inside of your SceneDelegate:
let viewModel = ViewModel(connectivityProvider: ConnectivityProvider())
let contentView = ContentView(viewModel: viewModel)
...
window.rootViewController = UIHostingController(rootView: contentView)
==================================
Update: How to activate the Session?
First add a new function to your ConnectivityProvider that activates the session:
class ConnectivityProvider: NSObject, WCSessionDelegate {
...
func connect() {
guard WCSession.isSupported() else {
print("WCSession is not supported")
return
}
session.activate()
}
...
}
Now you can call the connect function whenever you need your WCSession to be connected. You should be able to connect it everywhere, like in your SceneDelegate, inside your ViewModel, or even directly inside of the init of your ConnectivityProvider:
ConnectivityProvider init:
class ConnectivityProvider: NSObject, WCSessionDelegate {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
self.connect()
}
...
}
ViewModel:
import SwiftUI
final class ViewModel: ObservableObject {
private(set) var connectivityProvider: ConnectivityProvider
var textFieldValue: String = ""
init(connectivityProvider: ConnectivityProvider) {
self.connectivityProvider = connectivityProvider
self.connectivityProvider.connect()
}
func sendMessage() -> Void {
let txt = textFieldValue
let message = ["message":txt]
connectivityProvider.send(message: message)
}
}
SceneDelegate:
let connectivityProvider = ConnectivityProvider()
connectivityProvider.connect()
let viewModel = ViewModel(connectivityProvider: connectivityProvider)
let contentView = ContentView(viewModel: viewModel)
...
window.rootViewController = UIHostingController(rootView: contentView)