I want to run some code (fetching records to populate a List) when permission is granted for location access.
Here is my Location Manager:
import Foundation
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
#Published var locationStatus: CLAuthorizationStatus?
override init() {
super.init()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationStatus = status
print("This is the Model")
print(status == .authorizedWhenInUse)
}
}
And here is my view:
import SwiftUI
import CoreLocation
struct HorizontalPerformanceListView: View {
#ObservedObject var locationManager: LocationManager = LocationManager()
var performanceSet: PerformanceSet
var body: some View {
VStack (spacing: 10) {
//iterate over a fetched records
}
.onReceive(locationManager.$locationStatus) { _ in self.runCode() }
}
func runCode(){
print("This is the View")
print(locationManager.locationStatus == .authorizedWhenInUse)
if(locationManager.locationStatus == .authorizedWhenInUse){
print("run code here")
}
}
}
When the user prompted for location permission and specifies "allow while using app", I'd expect the "RunCode" func to run and for locationManager.locationStatus to be set to .authorizedWhenInUse.
The console prints instead:
This is the View
false
This is the Model
true
This is the View
false
This is the Model
true
The locationStatus is clearly changing in the model when permission is granted but my view still thinks access isn't there.
Clearly I am doing something wrong (I am somewhat new to SwiftUI/Combine) - I'd appreciate any help.
Change #ObservedObject to #StateObject. #StateObject is for data your view owns (i.e. instantiates), #ObservedObject is for data not owned by your view (i.e. passed in as an argument to your view).
Additionally, the publisher is an objectWillChange notification (i.e. it hasn't changed yet). Try using an asyncAfter delay to observe it correctly.
import SwiftUI
import CoreLocation
struct HorizontalPerformanceListView: View {
#StateObject var locationManager: LocationManager = LocationManager()
var performanceSet: PerformanceSet
var body: some View {
VStack (spacing: 10) {
//iterate over a fetched records
}
.onReceive(locationManager.$locationStatus) { _ in self.runCode() }
}
func runCode(){
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("This is the View")
print(locationManager.locationStatus == .authorizedWhenInUse)
if(locationManager.locationStatus == .authorizedWhenInUse){
print("run code here")
}
}
}
}
Related
This is what I am trying to achieve:
class MyVC: UIViewController {
#State var myBoolState: Bool = false
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
myBoolState.sink { value in .... }.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(myBoolState: $myBoolState))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#Binding var myBoolState: Bool
var body: some View {
Button(action: {
myBoolState = true
}) {
Text("Push Me")
}
}
}
But the above of course does not compile.
So the question is: can I somehow declare a published property inside a view controller, pass it to a SwiftUI View and get notified when the SwiftUI view changes its value?
The #State wrapper works (by design) only inside SwiftUI view, so you cannot use it in view controller. Instead there is ObsevableObject/ObservedObject pattern for such purpose because it is based on reference types.
Here is a demo of possible solution for your scenario:
import Combine
class ViewModel: ObservableObject {
#Published var myBoolState: Bool = false
}
class MyVC: UIViewController {
let vm = ViewModel()
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
vm.$myBoolState.sink { value in
print(">> here it goes")
}.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(vm: self.vm))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#ObservedObject var vm: ViewModel
var body: some View {
Button(action: {
vm.myBoolState = true
}) {
Text("Push Me")
}
}
}
I'm currently developing an application using SwiftUI.
I'm trying to use a google AdMob reward Ad.
I made codes to show reward Ads referring to this article.
I'm trying to show an alert after I finish watching a reward Ad fully using a Bool state from RewardedAdDelegate class but It doesn't work...
How could I solve this problem?
Here are the codes:
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
RewardedAd()
.alert(isPresented: $adDelegate.adFullyWatched){
Alert(title: Text("reward Ad finished"),
message: Text("reward Ad finished"),
dismissButton: .default(Text("OK")))
}
}
}
RewardedAd.swift
import SwiftUI
import GoogleMobileAds
struct RewardedAd: View {
#EnvironmentObject var appState: AppState
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
if adDelegate.adLoaded && !adDelegate.adFullyWatched {
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
}
return Text("Load ad").onTapGesture {
self.adDelegate.loadAd()
}
}
}
RewardedAdDelegate
import Foundation
import GoogleMobileAds
class RewardedAdDelegate: NSObject, GADRewardedAdDelegate, ObservableObject {
#Published var adLoaded: Bool = false
#Published var adFullyWatched: Bool = false
var rewardedAd: GADRewardedAd? = nil
func loadAd() {
rewardedAd = GADRewardedAd(adUnitID: "ca-app-pub-3940256099942544/1712485313")
rewardedAd!.load(GADRequest()) { error in
if error != nil {
self.adLoaded = false
} else {
self.adLoaded = true
}
}
}
/// Tells the delegate that the user earned a reward.
func rewardedAd(_ rewardedAd: GADRewardedAd, userDidEarn reward: GADAdReward) {
adFullyWatched = true
print("Reward received with currency: \(reward.type), amount \(reward.amount).")
}
/// Tells the delegate that the rewarded ad was presented.
func rewardedAdDidPresent(_ rewardedAd: GADRewardedAd) {
self.adLoaded = false
print("Rewarded ad presented.")
}
/// Tells the delegate that the rewarded ad was dismissed.
func rewardedAdDidDismiss(_ rewardedAd: GADRewardedAd) {
print("Rewarded ad dismissed.")
}
/// Tells the delegate that the rewarded ad failed to present.
func rewardedAd(_ rewardedAd: GADRewardedAd, didFailToPresentWithError error: Error) {
print("Rewarded ad failed to present.")
}
}
UPDATED
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
VStack{
RewardedAd(adDelegate: self.adDelegate)
.alert(isPresented: $adDelegate.adFullyWatched){
Alert(title: Text(""),
message: Text(""),
dismissButton: .default(Text("OK")))
}
// check adDelegate.adFullyWatched state -> after Ad finish this text shows
if adDelegate.adFullyWatched{
Text("Checked")
}
}
}
}
RewardedAd.swift
import SwiftUI
import GoogleMobileAds
struct RewardedAd: View {
#ObservedObject var adDelegate : RewardedAdDelegate
var body: some View {
if adDelegate.adLoaded && !adDelegate.adFullyWatched {
let root = UIApplication.shared.windows.first?.rootViewController
self.adDelegate.rewardedAd!.present(fromRootViewController: root!, delegate: adDelegate)
}
return Text("Load ad").onTapGesture {
self.adDelegate.loadAd()
}
}
}
ReUPDATED
AdShow.swift
import SwiftUI
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
#State var isAlert = false
var body: some View {
VStack{
Button(action: {
self.isAlert = true
}){
Text("alert")
.padding()
}
RewardedAd(adDelegate: self.adDelegate)
.alert(isPresented: self.$isAlert){
Alert(title: Text(""),
message: Text(""),
dismissButton: .default(Text("OK")))
}
// check adDelegate.adFullyWatched state -> after Ad finish this text shows
if adDelegate.adFullyWatched{
Text("Checked")
}
}
.onAppear(){
if adDelegate.adFullyWatched{
self.isAlert = true
}
}
}
}
Xcode: Version 12.0.1
iOS: 13.0
You use different instances of delegate in your views, instead you have to inject delegate from first view into second one.
struct AdShow: View {
#ObservedObject var adDelegate = RewardedAdDelegate()
var body: some View {
RewardedAd(adDelegate: self.adDelegate) // << here inject !!
// ... other code
and
struct RewardedAd: View {
#EnvironmentObject var appState: AppState
#ObservedObject var adDelegate: RewardedAdDelegate // << here declare !!
// ... other code
The root view of my onboarding process has a NavigationView. The root view of my app is a login page that also contains a NavigationView. That means when someone launches the app for the first time, they will go through the onboarding process and land at the login screen - resulting in a navigation view within a navigation view.
Is there a way to reset the view stack or simply remove the extra navigation view when necessary?
This is how I implemented #New Dev's solution below. First comes the Tower class. (The name helps me visualize the fact that it's an ObservableObject.) Its job is to keep track of the currentPage and let interested views know when it has changed.
import Foundation
import SwiftUI
import Combine
class Tower: ObservableObject {
enum Views {
case onboarding, login, dashboard
}
let objectWillChange = PassthroughSubject<Tower, Never>()
#Published var currentPage: Views = .onboarding {
didSet {
objectWillChange.send(self)
}
}
}
Next comes the ConductorView. It is notified by the Tower when currentPage changes, and loads the corresponding view.
struct ConductorView: View {
#EnvironmentObject var tower: Tower
var body: some View {
VStack {
if tower.currentPage == .onboarding {
ContentViewA()
} else if tower.currentPage == .login {
ContentViewB()
}
}
}
}
And lastly, a content view.
struct ContentViewA: View {
#EnvironmentObject var tower: Tower
var body: some View {
Button(action: {
self.tower.currentPage = .login
}) {
Text("Go to Login")
}
}
}
}
In addition to New Devs greatly appreciated solution, I also used this article from BLCKBIRDS.
I'll expand on my comment. NavigationView/NavigationLink aren't the only ways to change views - a simple conditional can also be used to determine which view is rendered.
So, say, you have some class that contains the state of the login/onboarding information:
class AppState: ObservableObject {
enum UserFlow {
case onboarding, login, home
}
#Published var userFlow: UserFlow = .onboarding
// ...
}
Then your RootView could determine which user flow to show:
struct RootView: View {
#EnvironmentObject var appState: AppState
var body: some View {
if appState.userFlow == .onboarding {
OnboardingRootView()
} else if appState.userFlow == .login {
LoginRootView()
} else {
ContentView()
}
}
}
I am trying to rewrite my app using SwiftUI only and I am having difficulty with the EnvironmentObject, trying to understand how it works…
I want to redirect my app users to the appropriate page at launch, depending on:
if this is their first time
if they have a login,
if they want to start using without login
If it is the first time the app is launched, LocalStorage has no data so I present the app on a welcome page
I offer the choice of 2 buttons to click on:
“New User” which redirect to the main page of the app and create a new user
“Login” which present the login page to retrieve the last backup
If the app has previously been launched, I present the main page straight away.
Now said, if I initiate my “currentPage” as “MainView” or “LoginView”, it works - but NOT if it is set as “WelcomeView”.
I presume the problem comes when the variable gets changed from a subview? I thought the use of #EnvironmentObject was the way to get around this…
Can someone explain to me how it works?
My various files are:
import SwiftUI
import Combine
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "WelcomeView" {
didSet {
objectWillChange.send(self)
}
}
}
import SwiftUI
struct ParentView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "WelcomeView" {
WelcomeView()
}
else if viewRouter.currentPage == "MainView" {
MainView()
}
else if viewRouter.currentPage == "LoginView" {
LoginView()
}
}
}
}
import SwiftUI
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons().environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}
import SwiftUI
struct LoginButtons: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {
self.viewRouter.currentPage = "MainView"
}) {
Text("NEW USER")
}
Button(action: {
self.viewRouter.currentPage = "LoginView"
}) {
Text("I ALREADY HAVE AN ACCOUNT")
}
}
}
}
import SwiftUI
struct MainView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
import SwiftUI
struct LoginView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
// Just want to check if it is working for now before implementing the appropriate Views...
Button(action: {
self.viewRouter.currentPage = "WelcomeView"
}) {
Text("BACK")
}
}
}
}
Many Thanks in advance! :wink:
Ok so in your main view, the one that you are going to decide where to send your user, you could check for the app if it was lunched before or not, depending on that do whatever you want. Once you know how to do this, you can adapt to the other things. This is how you can check for it, again, in your main view router:
init() {
// Create initial Data if not data has been setup
if (InitialAppSetup().initialDataLoaded == false) {
InitialAppSetup().createInitialData()
}
// Onboarding screen
if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
UserDefaults.standard.set(true, forKey: "didLaunchBefore")
currentPage = "onboardingView"
} else {
currentPage = "homeView"
}
}
The InitialAppSetup() class has a UserDefault which goes like this:
#Published var initialDataLoaded: Bool = UserDefaults.standard.bool(forKey: "InitialData") {
didSet {
UserDefaults.standard.set(self.initialDataLoaded, forKey: "InitialData")
}
}
Ok... My 'mistake' was to add an extra ".environmentObject(ViewRouter())" when calling my subview "LoginButtons".
If I remove it, it works!.. But why?!?
struct WelcomeView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ZStack{
// VStack { [some irrelevant extra code here] }
VStack {
LoginButtons()
// --> .environmentObject(ViewRouter())
}
// VStack { [some irrelevant extra code here] }
}
}
}
28-07-2019. I still have a question about the code below. I would like to separate the data model out of the ContentView. So I made a separate file and added the class, like this:
import SwiftUI
import Combine
class User: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var username : String = "Jan" { willSet { willChange.send() }}
var password : String = "123456" { willSet { willChange.send() } }
var emailAddress : String = "jan#mail.nl" { willSet { willChange.send() } }
}
#if DEBUG
struct User_Previews: PreviewProvider {
static var previews: some View {
User()
.environmentObject(User())
}
}
#endif
This doesn't work however, I'm getting an error:
Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols
Error occurs on the .environmentObject(User()) line in # if DEBUG.
After watching some video's I made the following code (including changes for Xcode 11 beta 4). Tips from both answers from dfd and MScottWaller are already included in the code.
import Combine
import SwiftUI
class User: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var username = "Jan" { willSet { willChange.send() } }
var password = "123456" { willSet { willChange.send() } }
var emailAddress = "jan#mail.nl" { willSet { willChange.send() } }
}
struct ContentView: View {
#EnvironmentObject var user: User
private func buttonPressed() {
print(user.username) // in Simulator
}
var body: some View {
VStack {
TextField("Username", text: $user.username)
TextField("Password", text: $user.password)
TextField("Emailaddress", text: $user.emailAddress)
Button(action: buttonPressed) {
Text("Press me!")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(User())
}
}
#endif
But now onto the next part. If I have another view... how can I reference the data then? Since the source of truth is in the above ViewContent() view. The answer is:
import SwiftUI
struct DetailView: View {
#EnvironmentObject var user: User
var body: some View {
VStack {
TextField("Username", text: $user.username)
TextField("Password", text: $user.password)
TextField("Email", text: $user.emailAddress)
}
}
}
#if DEBUG
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(User())
}
}
#endif
Don't forget to edit the SceneDelegate (answer from dfd):
var user = User()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(user)
)
self.window = window
window.makeKeyAndVisible()
}
}
In your DetailView preview, don't for get to attach the environmentObject. See how I've added it in the PreviewProvider below. When you run the actual app, you'll want to do the same to you ContentView in the SceneDelegate
import SwiftUI
struct DetailView: View {
#EnvironmentObject var user: User
var body: some View {
HStack {
TextField("Username", text: $user.username)
Text("Hello world!")
}
}
}
#if DEBUG
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(User())
}
}
#endif
If the "source of truth" is User, and you've made it a BindableObject you just need to expose it best to make it available to the various views you want. I suggest #EnvironmentObject.
In your SceneDelegate, do this:
var user = User()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(user)
)
self.window = window
window.makeKeyAndVisible()
}
}
Now that a "stateful" instance of User is available to any View, you simply need to add:
#EnvironmentObject var user: User
To any/all vies that need to know about User.
BindableObject (for the most part) reserve memory for what you've denied. #ObjectBinding merely binds a view to what is in that part of memory (again, for the most part). And yes, you can do this for User in all views - but since you are instantiating it in ContentView? Nope.)! #EnvironmentObject makes it available to any views that need to access it.
Absolutely, you can use #ObjectBinding instead of an #EnvironmentObject, but so far,? I've never heard of a reason to do that.