Giter Club home page Giter Club logo

rxappstate's Introduction

RxAppState

CI Status Platform Version Carthage compatible SPM compatible Xcode Swift License Twitter Blog

A collection of handy RxSwift Observables that let you observe all the changes in your Application's state and your UIViewController view-related notifications.

About

Application states

In almost every app there is some code that you want to run each time a user opens the app. For example you want to refresh some data or track that the user opened your app.

UIApplicationDelegate offers two methods that you could use to run the code when the user opens the app: applicationWillEnterForeground and applicationDidBecomeActive. But either of these methods is not ideal for this case:

applicationWillEnterForeground is not called the first time your app is launched. It is only called when the app was in the background and then enters the foreground. At first launch the app is not in the background state so this methods does not get called.

applicationDidBecomeActive does get called when the app is launched for the first time but is also called when the app becomes active after being in inactive state. That happens everytime the user opens Control Center, Notification Center, receives a phone call or a system prompt is shown (e.g. to ask the user for permission to send remote notifications). So if you put your code in applicationDidBecomeActive it will not only get called when the user opens the app but also in all those cases mentioned above.

So to really run your code only when your user opens the app you need to keep track of the app's state. You would probably implement something like this:

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    var didEnterBackground = true
    ...
    func applicationDidEnterBackground(_ application: UIApplication) {
        didEnterBackground = true
    }
    func applicationDidBecomeActive(_ application: UIApplication) {
        if didEnterBackground {
            // run your code
            didEnterBackground = false
        }
    }
    ...
}

This is not a big problem, but it is not a very elegant approach. And you have to set the inital value of didEnterBackground to true to run your code after the first launch (see above), even if the app never has been to the background. Call me picky, but I don't like that.

RxAppState to the rescue!
With RxAppState you can simply do the following:

UIApplication.shared.rx.didOpenApp
    .subscribe(onNext: { _ in
        // run your code
    })
    .disposed(by: disposeBag)

This runs your code whenever the user opens the app. It includes the first launch of the app and ignores the cases when the app enters active state without having been in background state before (like when the user just opened Control Center or received a phone call)

And there is more!
You want to show your user a tutorial when he first launches the app? And you only want to show it after the first launch and never again? No problem:

UIApplication.shared.rx.firstLaunchOnly
    .subscribe(onNext: { _ in
        // run your code
    })
    .disposed(by: disposeBag)

You want to show your user what features are new when he opens the app for the first time after an update?

UIApplication.shared.rx.firstLaunchOfNewVersionOnly
    .subscribe(onNext: { version in
        let previousAppVersion = version.previous
        let currentAppVersion = version.current
        // show what has changed between
        // the previous and the current version
    })
    .disposed(by: disposeBag)

You want check the previous and the current app version each time the user opens the app?

UIApplication.shared.rx.appVersion
    .subscribe(onNext: { version in
        let previousAppVersion = version.previous
        let currentAppVersion = version.current
        // run your code
    })
    .disposed(by: disposeBag)

You want to keep track of how many times the user has opened your app? Simply do this:

UIApplication.shared.rx.didOpenAppCount
    .subscribe(onNext: { count in
        print("app opened \(count) times")
    })
    .disposed(by: disposeBag)

The cherry on top:
This code does not have to live in your AppDelegate. You could put it anywhere you like in your app! So don't clutter your AppDelegate with this code, put it somewhere else!

ViewController view-related notifications

You can also use Observables to subscribe to your view controllers' view-related notifications:

Do do something when your view controller's viewDidAppear: method is called you can do this in your view controller class:

rx.viewDidAppear
    .subscribe(onNext: { animated in
       // do something
    })
    .disposed(by: disposeBag)

If you want to do something only when the view appeared for the first time you can easily do it like this:

rx.viewDidAppear
    .take(1)
    .subscribe(onNext: { animated in
       // do something
    })
    .disposed(by: disposeBag)

You can also directly bind you view controller's view state to another object:

rx.viewWillDisappear
    .bind(to: viewModel.saveChanges)
    .disposed(by: disposeBag)

Example

There is a simple example project to demonstrate how to use RxAppDelegate.

Requirements

iOS 13
Swift 5.2 Xcode 15 If you are still on Xcode 14 you can try to use the branch `/fix/xcode14-viewIsAppearing' (at own risk)

If you are using Swift 4.0 please use RxAppState version 1.1.1
If you are using Swift 4.1 please use RxAppState version 1.1.2
If you are using Swift 4.2 please use RxAppState version 1.4.1

Dependencies

RxSwift 6.2 or greater RxCocoa 6.2 or greater

Integration

CocoaPods

RxAppState is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod "RxAppState"

If Xcode complains about Swift versions add this to the end of your Podfile:

post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['SWIFT_VERSION'] = '5.0'
        end
    end
end

Carthage

You can use Carthage to install RxAppState by adding it to your Cartfile:

github "pixeldock/RxAppState"

Swift Package Manager

You can use Swift Package Manager to install RxAppState:

https://swift.org/package-manager/

Author

Jörn Schoppe,
[email protected]

Twitter Blog

License

RxAppState is available under the MIT license. See the LICENSE file for more info.

rxappstate's People

Contributors

arturdryomov avatar greatsk55 avatar hebdavepaul0 avatar ivanmkc avatar johnclayton avatar junmo-kim avatar kazukitanaka avatar krider2010 avatar neverwintermoon avatar pepasflo avatar pixeldock avatar pual avatar rdlgtmg avatar shenyj avatar svyatogor avatar tommyming avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rxappstate's Issues

Crash while trying to forward method from RxApplicationDelegateProxy

I implemented UNUserNotificationCenterDelegate protocol in my AppDelegate,
then use UIApplication.shared.rx.appState.map...etc

Then after getting push from system RxApplicationDelegateProxy crashed with unrecognized selector at [RxAppState.RxApplicationDelegateProxy userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:]

After a bit of debugging it seems that forward delegate in RxCocoa's DelegateProxy is nil

screen shot 2017-11-23 at 14 36 51

about "didFinishLaunchingWithOptions"

I need to start each time a user app running some initialization work, is in "didFinishLaunchingWithOptions" runs some initialization work.
I found it wrong to use "isFirstLaunch" because it only works when the app is first installed, not every time it runs.

about "application(_ app: UIApplication, open url: URL, options: ..."

Hi,
I have to open a resource specified by a URL.
The delegate "UIApplicationDelegate.application(_:open:options:)" is never call when I use your rx extension. (work fine otherwise)

So I tried to add it inside your extension like this :
public var applicationOpenURL: Observable<AppState> { return delegate.methodInvoked(#selector(UIApplicationDelegate.application(_:open:options:))) .map { _ in .active } } but that seems doesn't work.

isFirstLaunchOfNewVersion with first launch

Hello,

I would like to have an Observable with the same behavior as isFirstLaunchOfNewVersion, but which also emits true on first app launch.
The appVersion base observable is not enought to achieve this. This is because the AppVersion struct does not help distinguishing between the very first launch and "regular" app launches.

The workaround I have so far is something like:

Observable.zip(isFirstLaunch,isFirstLaunchOfNewVersion) { $0 || $1 }

Do you have a better idea how one could achieve this?

Maybe AppVersion could include some additional let isFirstLaunch: Bool attribute? This way it would fully represent the app state.

Support for extensions

Currently the framework cannot be imported in an extension because of the presence of UIApplication.shared in UIApplication.Rx.swift

extension RxAppState {
    /**
     For testing purposes
     */
    internal static func clearSharedObservables() {
        objc_setAssociatedObject(UIApplication.shared,
                                 &_sharedRxAppStateKey,
                                 nil,
                                 .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

The problem would be resolved just by injecting the application in the method

internal static func clearSharedObservables(application: UIApplication) {
    objc_setAssociatedObject(application,
                             &_sharedRxAppStateKey,
                             nil,
                             .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}

Do not use UserDefaults#synchronise - it’s officially bad (and potentially harmful)

At least since iOS 11 release last year, Apple recommends against using this - ever! They say: "this method is unnecessary and shouldn't be used" (https://developer.apple.com/documentation/foundation/userdefaults/1414005-synchronize)

Apple guarantees the synchronization will happen itself when needed. Using synchronize is bad because it's blocking and it writes to storage not just one change, but the whole dictionary which shouldn't be big, but can be. I still see a lot of guides on UserDefaults sort of advertising the synchronize and it's a pity. It became an anti-pattern.

In my opinion, this line should just be removed.

Please add PrivacyInfo

Hi, beginning with Mai 1st 2024 Apple requires that 3rd party SDK must habe a PrivacyInfo manifest included into there code.

https://developer.apple.com/support/third-party-SDK-requirements/
https://developer.apple.com/documentation/bundleresources/privacy_manifest_files

I've scanned my app with the following script (https://github.com/Wooder/ios_17_required_reason_api_scanner) for traces of code that requires action and your project was mentioned because if the usage of UserDefaults.

Can you please check it yourself and add a PrivacyInfo file?

Found potentially required reason API usage 'UserDefaults' in '...../Pods/RxAppState/Pod/Classes/UIApplication+Rx.swift'Line numbers: 91 305 319 325 336 Found potentially required reason API usage 'NSUserDefaults' in '...../Pods/RxAppState/Pod/Classes/UIApplication+Rx.swift' Line numbers: 91

Carthage Support only iOS 10.3

When using Carthage I get this error

MyProject/Classes/Models/Helpers/Helper.swift:11:8: Module file's minimum deployment target is ios10.3 v10.3: MyProject/Carthage/Build/iOS/RxAppState.framework/Modules/RxAppState.swiftmodule/x86_64.swiftmodule

Can you modify the deployment target for your module please ?

tvOS

Good to have tvOS support

isFirstLaunchOfNewVersion should be shared?

Hello,

first of all, thank you very much for your very convenient code!
I have a question about the Observables related to version number, like isFirstLaunchOfNewVersion.
My question is: would it be safer to share these observables by default, using a .share() operator?
The point is: if I subscribe twice to an observable like isFirstLaunchOfNewVersion, its side-effect code on the UserDefaults will be run twice, so one of the subscription will not work as expected.

Thank you very much,

Philippe

Potentially invalid App state if all Observables are not subscribed

Hello,

thank again for this great Pod!
I think there is a potential issue if you do not subscribe to all RxAppState Observable.
In my case, I have been using RxAppState since the public release of my App.
But first we only used the didOpenApp alike Observables, not the ones that involves UserDefaults such as isFirstLaunch, isFirstLaunchOfNewVersion, etc.
Therefore, if I start using isFirstLaunch in a new version of the App, then they will all trigger something wrong since the UserDefaults memory were not filled in the previous App versions since I never subscribed to them.

So I wonder whether it would not be better if RxAppState internally listen to AppDelegate notification and update its UserDefault memory internally, even if no Observables are subscribed.

I would be happy to have your opinion on this.

SPM Error

SPM Error is fixed in master branch, but there is no tag for it (probably 1.6.1)

Unable to call AppDelegate

I am having an issue since I installed the RxAppState framework. I can no longer call this

 if let appDele = UIApplication.shared.delegate as? AppDelegate {
            
        }

This always fails now. If I try to force unwrap, it crashes with following error

Could not cast value of type 'RxAppState.RxApplicationDelegateProxy' (0x7f95fae20b20) to 'CustomApp.AppDelegate' (0x1099ec9e8).

Could anyone tell me a workaround to this issue?

RxSwift 6.0

Hello mates!, could You please update your Rx dependencies to 6.0 version

Upgrade to latest RxSwift version

Hello,

could it be possible to use latest RxSwift version in pod dependencies? This would not require any change in the code.

Thanks!

Can't access AppDelegate

I need to access my AppDelegate, but when I do this let appDelegate = UIApplication.shared.delegate as! AppDelegate, an error occurs that I can't cast RxAppState.RxApplicationDelegateProxy to AppDelegate.

iOS 13 UISceneDelegate Support

Any interest in adding UISceneDelegate support? It seems that when you adopt the UISceneDelegate, some app lifecycle methods don't get called, instead the corresponding SceneDelegate methods are called. Example, sceneDidBecomeActive instead of applicationDidBecomeActive.

Crash unrecognized selector sent to instance

It was easy recreate this crash if you have objective-c app with code like this in app delegate

@implementation AppDelegate

+ (instancetype)sharedDelegate {
    return (AppDelegate*)[UIApplication sharedApplication].delegate;
}

Now any calls sharedDelegate in objective-c will return RxApplicationDelegateProxy and crash when you try access to any property of AppDelegate.

2019-08-17 21:20:08.183948+0300 CS DEV[3789:29536] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RxAppState.RxApplicationDelegateProxy managedObjectContext]: unrecognized selector sent to instance 0x600003ed3240'

Originally posted by @Tayphoon in #11 (comment)

FirstLaunchOfNewVersion

Hello,

I want to update information when user opens the app for the first time after a specific update, so I'm subscribing to firstLaunchOfNewVersionOnly as specified in the doc. But I never enter it even if I've changed the version number from 5.2 to 6.0. I'm using the version 1.1.0 of RxAppState.

Do you have any idea please ?

Add previous/current version to Observables

When using firstLaunchOfNewVersionOnly, I'd like to tell know the previous and current versions of the app.

The intent is to give a user a "What's new" type into if the version was prior to x.x.x. If the previous version was later, the "What's new" flow will be skipped. This would help to determine if the user has previously been introduced to a new feature, and to avoid repeatedly introducing said feature.

If firstLaunchOfNewVersionOnly was of type Observable<(String, String)>, subscribers to next events would receive the required information. This would be simpler than retrieving the Strings from UserDefaults manually.

Potentially an issue with viewState if it is not subscribed immediately

(this issue is quite similar to #18)

Hello,

thanks again for this Pod. I have started using its UIViewController methods which are really great – in my opinion they definitely should be part of the RxSwift/RxCocoa "standard library".

I think that viewState behavior might be surprising in the case it is not subscribed to at the very beginning of the UIViewController creation. Indeed, for instance, if is subscribed to after the base method viewDidAppear() is invoked for the first, time, then viewState will not emit .viewDidAppear immediately, but will emit nothing until viewWillDisappear() is invoked.

Maybe viewState should behave like a state, not just like a combination of events: any time some observer subscribes to it, it should immediately get an event true or false.
To do so, there would be two things to do:

  • using a .share(replay: 1, scope: .forever) and a .startWith(xx).
  • subscribing it internally, so that is begins immediately to listen for the viewDidAppear , viewWillDisappear and similar Observables, even if externally there are no subscribers to it. Maybe using a .publish() and .connect() to do this kind of "pre-fetch", or using an internal DisposeBag property (maybe with something like NSObject-Rx?)

Thank you very much, I would be pleased to know your opinion on this.

P.S. : And to go a bit further, it might be interesting to expose it as a Driver instead of an Observable.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.