Sheet not dismissing with presentation mode in SwiftUI/UIKit? - ios

I have a SwiftUI view that displays a sheet using a #State variable:
import SwiftUI
struct AdRevenue: View {
#State var playAd = false
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
self.playAd = true
})
{
Text("Play Ad")
}.sheet(isPresented: $playAd) {
Ads()}
}
}
This is the UIViewRepresentable sheet:
struct Ads: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
return ViewController()
}
func updateUIViewController(_ uiView: UIViewController, context: Context) {
}
class ViewController: UIViewController, GADRewardedAdDelegate, AdManagerRewardDelegate {
var rewardedAd: GADRewardedAd?
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
override func viewDidLoad() {
super.viewDidLoad()
AdManager.shared.loadAndShowRewardAd(AdIds.rewarded.rawValue, viewController: self)
AdManager.shared.delegateReward = self
}
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
print("Reward received: \(reward.type), amount \(reward.amount).")
}
}
}
Within AdManager, a function is called as such:
func rewardAdDidClose() {
let mom = Ads()
mom.presentationMode.wrappedValue.dismiss()
print("mom.presentationMode.wrappedValue.dismiss()")
}
Yet although I see the presentationMode message when I run it, the sheet doesn't get dismissed. Is it possible to dismiss the sheet like this?

Is this because you have two Ads values?
The first is created by SwiftUI (inside the AdRevenue view), so its presentationMode property is likely to be correctly wired-up to the app's environment.
But later, within AdManager, you're creating another Ads value, and expecting it to have a usable presentationMode environment object. It's being created outside of SwiftUI, so cannot know about the environment of the rest of your app.
I'd try passing the Ads value into your AdManager, rather than having AdManager create a new one.

Related

Swift UI StateObject Published two side communication

I am new to this SwiftUI and would like to ask some questions.
There are three objects in total.
View1, View2, Object1.
Here's part of my code.
struct View1: View {
#StateObject var object = Object1()
var body: some View {
VStack() {
View2(object: object)
Button(action: {} )
}
}
func isOverchanged() { //doSomething }
}
struct View2: UIViewControllerRepresentable {
#ObservedObject var object = Object1()
}
class Object1: NSObject, ObservableObject {
#Published var mydata: [Double] = []
#State var isOver: Bool = false
}
I want to achieve two goals.
The first one is that "view2" to be notified if there is a change in myData inside "object".
The second one is that isOverChanged function in "view1" to be notified (Or notify the button action closure) if the isOver inside the object changes.
The first goal has been achieved.
But the second one tried a lot without success.
If it is the way of writing ViewController delegate, I can set delegate.onIsOverChanged(). But when it comes to SwiftUI I don't know what to do.
Here is part of the delegate version that may looks like.
protocol ObjectProtocol {
onMyDataChanged()
onIsOverChanged()
}
class View1: UIViewController, ObjectProtocol {
var object: Object1 = Object(delegate: self)
var view2: View2 = View2()
func onMyDataChanged() {
view2.doSomething()
}
func onIsOverChanged() {
//doSomething
}
}
class Object1: NSObject {
weak var delegate: ObjectProtocl?
var isOver = false
func isOverChanged(){
delegate?.onIsOverChanged()
}
func myDataChanged(){
delegate?.onMyDataChanged()
}
}
class View2: UIView {
func doSomething(){ }
}
Really thanks.

SwiftUI View init called multiple times

I am pretty new to SwiftUI. I have a very simple view. It's just a root view that contains a WKWebView wrapped in a UIViewRepresentable. My problem is, that the init method of the UIViewRepresentable is called 6 times when the view is opened. Which means the WKWebView is initialised 6 times and all my initialisation code (setting JS callbacks, ...) is called 6 times. I added print statements to the init functions of the root view MyWebView and the subview WebView (the UIViewRepresentable). The root view init is only called once, but the subview's init is called 6 times. Is this normal? Or am I doing something wrong?
struct MyWebView: View {
#ObservedObject private var viewModel = WebViewModel()
init() {
print("root init")
}
var body: some View {
VStack(alignment: .leading, spacing: 0, content: {
WebView(viewModel: viewModel)
})
.navigationBarTitle("\(viewModel.title)", displayMode: .inline)
.navigationBarBackButtonHidden(true)
} }
struct WebView: UIViewRepresentable {
var wkWebView: WKWebView!
init(viewModel: WebViewModel) {
print("webview init")
doWebViewInitialization(viewModel: viewModel)
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
let request = URLRequest(url: URL(string: "https://www.google.com")!, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
return wkWebView
}
}
I'm not getting your issue of multiple calls to the init method of the UIViewRepresentable.
I modified slightly your WebView, and this is how I tested my answer:
import SwiftUI
import Foundation
import WebKit
#main
struct TestSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyWebView()
}.navigationViewStyle(StackNavigationViewStyle())
}
}
// for testing
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
let wkWebView = WKWebView()
init(viewModel: WebViewModel) {
print("\n-----> webview init")
// doWebViewInitialization(viewModel: viewModel)
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack(alignment: .leading, spacing: 0, content: {
WebView(viewModel: viewModel)
})
.navigationBarTitle("\(viewModel.title)", displayMode: .inline)
.navigationBarBackButtonHidden(true)
}
}
This leaves "doWebViewInitialization" with a possible problem spot.
You have to write your code assuming that the initializer of the View in SwiftUI will be called many times.
You write the initialization process in makeUIView(context:) in this case.
See:
https://developer.apple.com/documentation/swiftui/uiviewrepresentable/makeuiview(context:)
For example, I wrote the following code based on this answer. I added a toggle height button to this referenced code.
the -----> makeUIView log is only output once,
but the -----> webview init logs are output every time the toggle button is pressed.
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
MyWebView()
}
}
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
let wkWebView = WKWebView()
init(viewModel: WebViewModel) {
print("\n-----> webview init")
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
print("\n-----> makeUIView")
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#State private var toggleHight = false
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack {
WebView(
viewModel: viewModel
)
.frame(
height: { toggleHight ? 600 : 300 }()
)
Button(
"toggle",
action: {
toggleHight.toggle()
}
)
}
}
}
Furthermore, I realized after I wrote example code that WebView: UIViewRepresentable should not have an instance variable of wkWebView.
Please do it all(create instance and configuration) in makeUIView(context:), as shown below.
This is because instance variables are recreated every time the initializer is called.
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
MyWebView()
}
}
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
init(viewModel: WebViewModel) {
print("\n-----> webview init")
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
print("\n-----> makeUIView")
let wkWebView = WKWebView()
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#State private var toggleHight = false
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack {
WebView(
viewModel: viewModel
)
.frame(
height: { toggleHight ? 600 : 300 }()
)
Button(
"toggle",
action: {
toggleHight.toggle()
}
)
}
}
}
I struggled with this tight constraint when I was developing with UIViewControllerRepresentable. With the help of my colleagues, I managed to finish the code.
Your code has been called 6 times, so there may be some problem. but I cannot tell what the problem is from the code you provided.
It is common for init to be called multiple times in SwiftUI. We need to write code to deal with this. If your init is being called too often, you may want to look for the root cause. The code I referred to and the code I wrote are only once at startup.

Pass variable from UIViewController to SwiftUI View

I can't find a way or a good tutorial to explain how to pass the value of a variable (String or Int) that is owned by a UIViewController to a SwiftUI view that is calling the view.
For example:
class ViewController: UIViewController {
var myString : String = "" // variable of interest
....
func methodThatChangeValueOfString(){
myString = someValue
}
}
// to make the view callable on SwiftUI
extension ViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<ViewController>) -> ViewController {
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: UIViewControllerRepresentableContext<ViewController>) {
}
}
In SwiftUI I'll have
struct ContentView: View {
var body: some View {
ViewController()
}
}
How can I take myString of the ViewController and use it in ContentView?
Thanks in advance
Use MVVM pattern it is what is recommended with SwiftUI.
Share a ViewModel between your SwiftUI View and your UIKit ViewController.
I suggest you start with the basic Apple SwiftUI tutorials. Specifically how to "Interface with UIKit"
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
import SwiftUI
struct SwiftUIView: View {
#StateObject var sharedVM: SharedViewModel = SharedViewModel()
var body: some View {
VStack{
UIKitViewController_UI(sharedVM: sharedVM)
Text(sharedVM.myString)
}
}
}
class SharedViewModel: ObservableObject{
#Published var myString = "init String"
}
//Not an extension
struct UIKitViewController_UI: UIViewControllerRepresentable {
typealias UIViewControllerType = UIKitViewController
var sharedVM: SharedViewModel
func makeUIViewController(context: Context) -> UIKitViewController {
return UIKitViewController(vm: sharedVM)
}
func updateUIViewController(_ uiViewController: UIKitViewController, context: Context) {
}
}
class UIKitViewController: UIViewController {
let sharedVM: SharedViewModel
var runCount = 0
init(vm: SharedViewModel) {
self.sharedVM = vm
super.init(nibName: nil, bundle: nil)
//Sample update mimics the work of a Delegate or IBAction, etc
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.runCount += 1
self.methodThatChangeValueOfString()
if self.runCount == 10 {
timer.invalidate()
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func methodThatChangeValueOfString(){
sharedVM.myString = "method change" + runCount.description
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}

SwiftUI with UIViewControllerRepresentable

I am trying to use a UIViewController representable in a swiftUi project. Specifically I am trying to press one button (assetOne) that allows the EU to select a video and then press another button (assetTwo) and it allows the user to select another video. Then the user will have the option to merge the videos (with a third button). I assumed that I would need to use a Coordinator to accomplish this but after seeing a SO solution without it I tried to do it without one. But when I run my project the build is successful but when I click on any of the buttons from the content view I get the error message below. What am I doing wrong? Do I need a Coordinator and how do I incorporate it with my current configuration?
Warning: Attempt to present <UIImagePickerController: 0x7fa05f827600>
on <TempTest.MergeVideoViewController: 0x7fa05ed088c0> whose view is
not in the window hierarchy!
Content View:
import SwiftUI
struct ContentView: View {
let someView = ImagePicker()
var body: some View {
VStack {
Button(action: {
print("SwiftUI: assetOne button tapped")
// Call func in SomeView()
self.someView.assetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
//...
ImagePicker: UIViewControllerRepresentable
struct ImagePicker: UIViewControllerRepresentable{
let someView = MergeVideoViewController()
func makeUIViewController(context: Context) -> MergeVideoViewController {
someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
func assetOne() {
someView.loadAssetOne()
}
//...
}
My UIViewController class:
class MergeVideoViewController: UIViewController {
var firstAsset: AVAsset?
var secondAsset: AVAsset?
var audioAsset: AVAsset?
var loadingAssetOne = false
var activityMonitor: UIActivityIndicatorView!
func exportDidFinish(_ session: AVAssetExportSession) {
// Cleanup assets
activityMonitor.stopAnimating()
firstAsset = nil
secondAsset = nil
audioAsset = nil
//...
func loadAssetOne() {
// func loadAssetOne(_ sender: AnyObject) {
if savedPhotosAvailable() {
loadingAssetOne = true
VideoHelper.startMediaBrowser(delegate: self, sourceType: .savedPhotosAlbum)
}
}
//...
The ImagePicker is-a View, it should be somewhere in body.
Here is possible approach - the idea is to get controller reference back in SwiftUI and call its actions directly when needed.
struct ImagePicker: UIViewControllerRepresentable{
let configure: (MergeVideoViewController) -> ()
func makeUIViewController(context: Context) -> MergeVideoViewController {
let someView = MergeVideoViewController()
configure(someView)
return someView
}
func updateUIViewController(_ uiViewController: MergeVideoViewController, context: Context) {}
}
struct ContentView: View {
#State private var controller: MergeVideoViewController?
var body: some View {
VStack {
ImagePicker {
self.controller = $0
}
Button(action: {
print("SwiftUI: assetOne button tapped")
self.controller?.loadAssetOne()
}) {
Text("Asset One").foregroundColor(Color.black)
}
.background(Color.blue)
.padding(10)
.clipShape(Capsule())
}
}
}

Change SwiftUI Text from UIViewController

I'm quite new to Swift and absolute new to SwiftUI.
I'm trying to combine UIViewController Google Maps and modern SwiftUI.
In SwiftUI i have a few Text objects, and I want my GmapsController class to be able to modify these Text values, and redraw SwiftUI struct.
My main struct:
var _swiftUIStruct : swiftUIStruct = swiftUIStruct() // to have access in GmapsController
struct GoogMapView: View {
var body: some View {
let gmap = GmapsControllerRepresentable()
return ZStack {
gmap
_swiftUIStruct
}
}
}
UIViewControllerRepresentable wrapper of GmapsController :
struct GmapsControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) -> GmapsController {
return GmapsController()
}
func updateUIViewController(_ uiViewController: GmapsController, context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) {}
}
The GmapsController itself:
class GmapsController: UIViewController, CLLocationManagerDelegate, GMSMapViewDelegate {
var locationManager = CLLocationManager()
var mapView: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
let camera = GMSCameraPosition.camera(
withLatitude: (locationManager.location?.coordinate.latitude)!,
longitude: (locationManager.location?.coordinate.longitude)!,
zoom: 15
)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
mapView.delegate = self
self.view = mapView
}
// HERE I WANT TO CHANGE SOME swiftUIStruct Text FIELDS
// calls after map move
func mapView(_ mapView: GMSMapView, idleAt сameraPosition: GMSCameraPosition) {
_swiftUIStruct.someTxt = "I hope this will work" // compilation pass, but value doesn't change
}
}
And the swiftUIStruct struct:
struct swiftUIStruct {
#State var someTxt = ""
var body: some View {
Text(someTxt) // THE TEXT I WISH I COULD CHANGE
}
}
Googling this a whole day just made me feel dumb, any help is appreciated.
I hope my example code helps. Basically, move the model data outside, and pass it along, and change it. If you run this code, you will see the text "I hope this will work", NOT "Initial Content".
import SwiftUI
class ViewModel: ObservableObject {
#Published var someTxt = "Initial Content"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ZStack {
GoogleMapsView(viewModel: viewModel)
Text(viewModel.someTxt)
}
}
}
struct GoogleMapsView: UIViewControllerRepresentable {
var viewModel: ViewModel
func makeUIViewController(context: Context) -> GmapsController {
let controller = GmapsController()
controller.viewModel = viewModel
return controller
}
func updateUIViewController(_ uiViewController: GmapsController, context: Context) {}
}
class GmapsController: UIViewController {
var viewModel: ViewModel?
override func viewDidLoad() {
viewModel?.someTxt = "I hope this will work"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}

Resources