If we go from Swift background to foreground, what is the proper way to [nsObject copy] in Swift?
For example in Objective-C, we would loop through a long array of ALAssets (say like 10,000+) in the background by doing:
[alGroup enumerateAssetsUsingBlock:^(ALAsset *alPhoto, NSUInteger index, BOOL *stop)
{
// Here to make changes for speed up image loading from device library...
// =====================================================
// >>>>>>>>>>>>>>>>>>> IN BACKGROUND <<<<<<<<<<<<<<<<<<<
// =====================================================
if(alPhoto == nil)
{
c(#"number of assets to display: %d", (int)bgAssetMedia.count);
// c(#"All device library photos uploaded into memory...%#", bgAssetMedia);
dispatch_async(dispatch_get_main_queue(), ^(void)
{
// =====================================================
// >>>>>>>>>>>>>>>>>>> IN FOREGROUND <<<<<<<<<<<<<<<<<<<
// =====================================================
[ui hideSpinner];
if (_bReverse)
// Here we copying all the photos from device library into array (_assetPhotos)...
_assetPhotos = [[NSMutableArray alloc] initWithArray:[[[bgAssetMedia copy] reverseObjectEnumerator] allObjects]];
else
_assetPhotos = [[NSMutableArray alloc] initWithArray:[bgAssetMedia copy]];
// NSLog(#"%lu",(unsigned long)_assetPhotos.count);
if (_assetPhotos.count > 0)
{
result(_assetPhotos);
}
});
} else {
// if we have a Custom album, lets remove all shared videos from the Camera Roll
if (![self isPhotoInCustomAlbum:alPhoto])
{
// for some reason, shared glancy videos still show with 00:00 minutes and seconds, so remove them now
BOOL isVideo = [[alPhoto valueForProperty:ALAssetPropertyType] isEqual:ALAssetTypeVideo];
int duration = 0;
int minutes = 0;
int seconds = 0;
// NSString *bgVideoLabel = nil;
if (isVideo)
{
NSString *strduration = [alPhoto valueForProperty:ALAssetPropertyDuration];
duration = [strduration intValue];
minutes = duration / 60;
seconds = duration % 60;
// bgVideoLabel = [NSString stringWithFormat:#"%d:%02d", minutes, seconds];
if (minutes > 0 || seconds > 0)
{
[bgAssetMedia addObject:alPhoto];
}
} else {
[bgAssetMedia addObject:alPhoto];
}
}
}
// NSLog(#"%lu",(unsigned long)bgAssetMedia.count);
}];
Then, we would switch to the foreground to update the UIViewController, which are these lines in the above snippet:
_assetPhotos = [[NSMutableArray alloc] initWithArray:[bgAssetMedia copy]];
The "copy" function was the black magic that allowed us to quickly marshal the memory from background to foreground without having to loop through array again.
Is there a similar method in Swift? Perhaps something like this:
_assetPhotos = NSMutableArray(array: bgAssetMedia.copy())
Is Swift thread safe now for passing memory pointers from background to foreground? What's the new protocol? Thank you-
I found the answer. After running large queries on the Realm and CoreData database contexts. I found it easy to just make a basic copy of the memory pointer and downcast it to match the class.
let mediaIdFG = mediaId.copy() as! String
Full example in context below:
static func createOrUpdate(dictionary:NSDictionary) -> Promise<Media> {
// Query and update from any thread
return Promise { fulfill, reject in
executeInBackground {
// c("BG media \(dictionary)")
let realm:RLMRealm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
let media = Media.createOrUpdateInRealm(realm, withJSONDictionary:dictionary as [NSObject : AnyObject])
// media.type = type
c("BG media \(media)")
let mediaId = media.localIdentifier
do {
try realm.commitWriteTransaction()
executeInForeground({
let mediaIdFG = mediaId.copy() as! String
let newMedia = Media.findOneByLocalIdentifier(mediaIdFG)
c("FG \(mediaIdFG) newMedia \(newMedia)")
fulfill(newMedia)
})
} catch {
reject( Constants.createError("Realm Something went wrong!") )
}
}
} // return promise
} // func createOrUpdate
Posting my own answer to let you know my findings. I also found this helpful article about Swift's copy() aka objc's copyWithZone: https://www.hackingwithswift.com/example-code/system/how-to-copy-objects-in-swift-using-copy
Related
Background
In my app, I have class called FavoritesController that manages objects that the user's marked as favorites, and this favorite status is then used throughout the app. The FavoritesController is designed as a singleton class as there are a number of UI elements throughout the app that needs to know the 'favorite status' for objects in different places, also network requests need to be able to signal that a favorite needs to be invalidated if the server says so.
This invalidation part happens when the server responds with a 404 error, indicating that the favorite object must be removed from the user's favorites. The network fetch function throws an error, which triggers the FavoritesController to remove the object and then send a notification to interested parties that they need to refresh.
The problem
When using a unit test to check the quality of the 404 implementation, all methods are triggered as intended – the error is thrown and caught, the FavoritesController deletes the object and sends the notification. In some instances though, the deleted favorite is still there – but it depends on from where the query is done!
If I query inside the singleton the deletion went OK, but if I query from a class that makes use of the singleton, the deletion didn't happen.
Design details
The FavoritesController property favorites uses an ivar with all accesses #synchronized(), and the value of the ivar is backed by a NSUserDefaults property.
A favorite object is an NSDictionary with two keys: id and name.
Other info
One weird thing which I fail to understand why it happens: in some deletion attempts, the name value for the favorite object gets set to "" but the id key retains its value.
I've written unit tests that add an invalid favorite and checks that it gets removed on first server query. This test passes when starting with empty set of favorites, but fails when there is an instance of 'semi-deleted' object as above (that retains its id value)
The unit test now consistently passes, but in live usage the failure to delete remains. I suspect that this is due to NSUserDefaults not saving to disk immediately.
Steps I've tried
Making sure that the singleton implementation is a 'true' singleton, i.e. sharedController always returns the same instance.
I thought there was some sort of 'capture' problem, where a closure would keep its own copy with outdated favorites, but I think not. When NSLogging the object ID it returns the same.
Code
FavoritesController main methods
- (void) serverCanNotFindFavorite:(NSInteger)siteID {
NSLog(#"Server can't find favorite");
NSDictionary * removedFavorite = [NSDictionary dictionaryWithDictionary:[self favoriteWithID:siteID]];
NSUInteger index = [self indexOfFavoriteWithID:siteID];
[self debugLogFavorites];
dispatch_async(dispatch_get_main_queue(), ^{
[self removeFromFavorites:siteID completion:^(BOOL success) {
if (success) {
NSNotification * note = [NSNotification notificationWithName:didRemoveFavoriteNotification object:nil userInfo:#{#"site" : removedFavorite, #"index" : [NSNumber numberWithUnsignedInteger:index]}];
NSLog(#"Will post notification");
[self debugLogFavorites];
[self debugLogUserDefaultsFavorites];
[[NSNotificationCenter defaultCenter] postNotification:note];
NSLog(#"Posted notification with name: %#", didRemoveFavoriteNotification);
}
}];
});
}
- (void) removeFromFavorites:(NSInteger)siteID completion:(completionBlock) completion {
if ([self isFavorite:siteID]) {
NSMutableArray * newFavorites = [NSMutableArray arrayWithArray:self.favorites];
NSIndexSet * indices = [newFavorites indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber * value = (NSNumber *)[entryUnderTest objectForKey:#"id"];
if ([value isEqualToNumber:[NSNumber numberWithInteger:siteID]]) {
return YES;
}
return NO;
}];
__block NSDictionary* objectToRemove = [[newFavorites objectAtIndex:indices.firstIndex] copy];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Will remove %#", objectToRemove);
[newFavorites removeObject:objectToRemove];
[self setFavorites:[NSArray arrayWithArray:newFavorites]];
if ([self isFavorite:siteID]) {
NSLog(#"Failed to remove!");
if (completion) {
completion(NO);
}
} else {
NSLog(#"Removed OK");
if (completion) {
completion(YES);
}
}
});
} else {
NSLog(#"Tried removing site %li which is not a favorite", (long)siteID);
if (completion) {
completion(NO);
}
}
}
- (NSArray *) favorites
{
#synchronized(self) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
return internalFavorites;
}
}
- (void) setFavorites:(NSArray *)someFavorites {
#synchronized(self) {
internalFavorites = someFavorites;
[self.defaults setObject:internalFavorites forKey:k_key_favorites];
}
}
- (void) addToFavorites:(NSInteger)siteID withName:(NSString *)siteName {
if (![self isFavorite:siteID]) {
NSDictionary * newFavorite = #{
#"name" : siteName,
#"id" : [NSNumber numberWithInteger:siteID]
};
dispatch_async(dispatch_get_main_queue(), ^{
NSArray * newFavorites = [self.favorites arrayByAddingObject:newFavorite];
[self setFavorites:newFavorites];
});
NSLog(#"Added site %# with id %ld to favorites", siteName, (long)siteID);
} else {
NSLog(#"Tried adding site as favorite a second time");
}
}
- (BOOL) isFavorite:(NSInteger)siteID
{
#synchronized(self) {
NSNumber * siteNumber = [NSNumber numberWithInteger:siteID];
NSArray * favs = [NSArray arrayWithArray:self.favorites];
if (favs.count == 0) {
NSLog(#"No favorites");
return NO;
}
NSIndexSet * indices = [favs indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
if ([[entryUnderTest objectForKey:#"id"] isEqualToNumber:siteNumber]) {
return YES;
}
return NO;
}];
if (indices.count > 0) {
return YES;
}
}
return NO;
}
Singleton implementation of FavoritesController
- (instancetype) init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype) sharedController
{
return [self new];
}
Unit testing code
func testObsoleteFavoriteRemoval() {
let addToFavorites = self.expectation(description: "addToFavorites")
let networkRequest = self.expectation(description: "network request")
unowned let favs = PKEFavoritesController.shared()
favs.clearFavorites()
XCTAssertFalse(favs.isFavorite(313), "Should not be favorite initially")
if !favs.isFavorite(313) {
NSLog("Adding 313 to favorites")
favs.add(toFavorites: 313, withName: "Skatås")
}
let notification = self.expectation(forNotification: NSNotification.Name("didRemoveFavoriteNotification"), object: nil) { (notification) -> Bool in
NSLog("Received notification: \(notification.name.rawValue)")
return true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
NSLog("Verifying 313 is favorite")
XCTAssertTrue(favs.isFavorite(313))
addToFavorites.fulfill()
}
self.wait(for: [addToFavorites], timeout: 5)
NSLog("Will trigger removal for 313")
let _ = SkidsparAPI.fetchRecentReports(forSite: 313, session: SkidsparAPI.session()) { (reports) in
NSLog("Network request completed")
networkRequest.fulfill()
}
self.wait(for: [networkRequest, notification], timeout: 10)
XCTAssertFalse(favs.isFavorite(313), "Favorite should be removed after a 404 error from server")
}
To give context around my answers, this is what the code in question looked like when suggesting the change:
- (NSArray *)favorites {
#synchronized(internalFavorites) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
I was suspicious of the check if (!internalFavorites) { that followed #synchronized(internalFavorites) because that meant that there was an expectation of #synchronized being passed nil, which results in a noop.
This meant multiple calls to to favorites or setFavorites could happen in funny ways since they wouldn't actually be synchronized. Giving #sychronized an actual object to synchronize on was crucial for thread safety. Synchronizing on self is fine, but for a particular class, you have to be careful not to synchronize too many things on self or you'll be bound to create needless blocking. Providing simple NSObjects to #sychronized is a good way to narrow the scope of what you're protecting.
Here's how you can avoid using self as your lock.
- (instancetype)init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.lock = [NSObject new];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype)sharedController {
return [self new];
}
- (NSArray *)favorites {
#synchronized(_lock) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
Regarding the abnormalities between test runs, definitely calling synchronize on the NSUserDefaults will help because calls to change the defaults are asynchronous, which means other threads are involved. There are like 3 layers of caching as well, and specifically for the purposes of running tests synchronize should ensure that things are completely and cleanly committed before Xcode pulls the plug on the test run. The documentation very abruptly insists that it's not a necessary call, but if it truly weren't necessary it wouldn't exist :-). On my first iOS projects, we always called synchronize after every defaults change... so, I think that documentation is more aspirational on the Apple engineers' parts. I'm glad this intuition helped you.
WebRTC video by default uses Front Camera, which works fine. However, i need to switch it to back camera, and i have not been able to find any code to do that.
Which part do i need to edit?
Is it the localView or localVideoTrack or capturer?
Swift 3.0
Peer connection can have only one 'RTCVideoTrack' for sending video stream.
At first, for change camera front/back you must remove current video track on peer connection.
After then, you create new 'RTCVideoTrack' on camera which you need, and set this for peer connection.
I used this methods.
func swapCameraToFront() {
let localStream: RTCMediaStream? = peerConnection?.localStreams.first as? RTCMediaStream
localStream?.removeVideoTrack(localStream?.videoTracks.first as! RTCVideoTrack)
let localVideoTrack: RTCVideoTrack? = createLocalVideoTrack()
if localVideoTrack != nil {
localStream?.addVideoTrack(localVideoTrack)
delegate?.appClient(self, didReceiveLocalVideoTrack: localVideoTrack!)
}
peerConnection?.remove(localStream)
peerConnection?.add(localStream)
}
func swapCameraToBack() {
let localStream: RTCMediaStream? = peerConnection?.localStreams.first as? RTCMediaStream
localStream?.removeVideoTrack(localStream?.videoTracks.first as! RTCVideoTrack)
let localVideoTrack: RTCVideoTrack? = createLocalVideoTrackBackCamera()
if localVideoTrack != nil {
localStream?.addVideoTrack(localVideoTrack)
delegate?.appClient(self, didReceiveLocalVideoTrack: localVideoTrack!)
}
peerConnection?.remove(localStream)
peerConnection?.add(localStream)
}
As of now I only have the answer in Objective C language in regard to Ankit's comment below. I will convert it into Swift after some time.
You can check the below code
- (RTCVideoTrack *)createLocalVideoTrack {
RTCVideoTrack *localVideoTrack = nil;
NSString *cameraID = nil;
for (AVCaptureDevice *captureDevice in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
if (captureDevice.position == AVCaptureDevicePositionFront) {
cameraID = [captureDevice localizedName]; break;
}
}
RTCVideoCapturer *capturer = [RTCVideoCapturer capturerWithDeviceName:cameraID];
RTCMediaConstraints *mediaConstraints = [self defaultMediaStreamConstraints];
RTCVideoSource *videoSource = [_factory videoSourceWithCapturer:capturer constraints:mediaConstraints];
localVideoTrack = [_factory videoTrackWithID:#"ARDAMSv0" source:videoSource];
return localVideoTrack;
}
- (RTCVideoTrack *)createLocalVideoTrackBackCamera {
RTCVideoTrack *localVideoTrack = nil;
//AVCaptureDevicePositionFront
NSString *cameraID = nil;
for (AVCaptureDevice *captureDevice in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
if (captureDevice.position == AVCaptureDevicePositionBack) {
cameraID = [captureDevice localizedName];
break;
}
}
RTCVideoCapturer *capturer = [RTCVideoCapturer capturerWithDeviceName:cameraID];
RTCMediaConstraints *mediaConstraints = [self defaultMediaStreamConstraints];
RTCVideoSource *videoSource = [_factory videoSourceWithCapturer:capturer constraints:mediaConstraints];
localVideoTrack = [_factory videoTrackWithID:#"ARDAMSv0" source:videoSource];
return localVideoTrack;
}
If you decide to use official Google build here the explanation:
First, you must configure your camera before call start, best place to do that in ARDVideoCallViewDelegate in method didCreateLocalCapturer
- (void)startCapture:(void (^)(BOOL succeeded))completionHandler {
AVCaptureDevicePosition position = _usingFrontCamera ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
__weak AVCaptureDevice *device = [self findDeviceForPosition:position];
if ([device lockForConfiguration:nil]) {
if ([device isFocusPointOfInterestSupported]) {
[device setFocusModeLockedWithLensPosition:0.9 completionHandler: nil];
}
}
AVCaptureDeviceFormat *format = [self selectFormatForDevice:device];
if (format == nil) {
RTCLogError(#"No valid formats for device %#", device);
NSAssert(NO, #"");
return;
}
NSInteger fps = [self selectFpsForFormat:format];
[_capturer startCaptureWithDevice: device
format: format
fps:fps completionHandler:^(NSError * error) {
NSLog(#"%#",error);
if (error == nil) {
completionHandler(true);
}
}];
}
Don't forget enabling capture device is asynchronous, sometime better to use completion to be sure everything done as expected.
I am not sure which chrome version you are using for webrtc but with v54 and above there is "bool" property called "useBackCamera" in RTCAVFoundationVideoSource class. You can make use of this property to switch between front/back camera.
Swift 4.0 & 'GoogleWebRTC' : '1.1.20913'
RTCAVFoundationVideoSource class has a property named useBackCamera that can be used for switching the camera used.
#interface RTCAVFoundationVideoSource : RTCVideoSource
- (instancetype)init NS_UNAVAILABLE;
/**
* Calling this function will cause frames to be scaled down to the
* requested resolution. Also, frames will be cropped to match the
* requested aspect ratio, and frames will be dropped to match the
* requested fps. The requested aspect ratio is orientation agnostic and
* will be adjusted to maintain the input orientation, so it doesn't
* matter if e.g. 1280x720 or 720x1280 is requested.
*/
- (void)adaptOutputFormatToWidth:(int)width height:(int)height fps:(int)fps;
/** Returns whether rear-facing camera is available for use. */
#property(nonatomic, readonly) BOOL canUseBackCamera;
/** Switches the camera being used (either front or back). */
#property(nonatomic, assign) BOOL useBackCamera;
/** Returns the active capture session. */
#property(nonatomic, readonly) AVCaptureSession *captureSession;
Below is the implementation for switching camera.
var useBackCamera: Bool = false
func switchCamera() {
useBackCamera = !useBackCamera
self.switchCamera(useBackCamera: useBackCamera)
}
private func switchCamera(useBackCamera: Bool) -> Void {
let localStream = peerConnection?.localStreams.first
if let videoTrack = localStream?.videoTracks.first {
localStream?.removeVideoTrack(videoTrack)
}
let localVideoTrack = createLocalVideoTrack(useBackCamera: useBackCamera)
localStream?.addVideoTrack(localVideoTrack)
self.delegate?.webRTCClientDidAddLocal(videoTrack: localVideoTrack)
if let ls = localStream {
peerConnection?.remove(ls)
peerConnection?.add(ls)
}
}
func createLocalVideoTrack(useBackCamera: Bool) -> RTCVideoTrack {
let videoSource = self.factory.avFoundationVideoSource(with: self.constraints)
videoSource.useBackCamera = useBackCamera
let videoTrack = self.factory.videoTrack(with: videoSource, trackId: "video")
return videoTrack
}
In the current version of WebRTC, RTCAVFoundationVideoSource has been deprecated and replaced with a
generic RTCVideoSource combined with an RTCVideoCapturer implementation.
In order to switch the camera I'm doing this:
- (void)switchCameraToPosition:(AVCaptureDevicePosition)position completionHandler:(void (^)(void))completionHandler {
if (self.cameraPosition != position) {
RTCMediaStream *localStream = self.peerConnection.localStreams.firstObject;
[localStream removeVideoTrack:self.localVideoTrack];
//[self.peerConnection removeStream:localStream];
self.localVideoTrack = [self createVideoTrack];
[self startCaptureLocalVideoWithPosition:position completionHandler:^{
[localStream addVideoTrack:self.localVideoTrack];
//[self.peerConnection addStream:localStream];
if (completionHandler) {
completionHandler();
}
}];
self.cameraPosition = position;
}
}
Take a look at the commented lines, If you start adding/removing the stream from the peer connection it will cause a delay in the video connection.
I'm using GoogleWebRTC-1.1.25102
So I have a service where I can ask for profile-json of a user by passing in the profileID in the request body. If I need 10 peoples profiles, I pass in 10 profileID, and the server will get back with all 10 profile information. Max count = 20.
So I have added an optimization algorithm that groups a bunch of profileIDs based on user scrolling really fast, or user opening up bunch of profiles in the app, etc... Basically my logic is that I have a dispatch_queue that waits roughly 0.6 seconds and in that time I have an array that collects all profileIDs different views in the app will ask for. After 0.6 seconds, whatever is queued up in the array, i include that in the request body, and send it to server.
Any better strategies/design patterns that I could use here?
My implementation looks like this.
----> call in the main thread <--------
CXAssert([NSThread isMainThread], #"profileInfoForUsers should be main thread");
if (!profileIDCacheWaitingToRequest) {
profileIDCacheWaitingToRequest = [NSMutableArray array];
}
if (!profileIDArraySubmittedToTheServer) {
profileIDArraySubmittedToTheServer = [NSMutableArray array];
}
if (!_profile_batch_process_queue) {
_profile_batch_process_queue = dispatch_queue_create("com.gather.chatx.profile.batch.process", DISPATCH_QUEUE_SERIAL);
}
NSMutableArray *filteredProfileIDCache = [NSMutableArray array];
for (NSString *profileID in users) {
if (profileID == NULL) {
continue;
}
if (![profileIDArraySubmittedToTheServer containsObject:profileID]) {
CXDebugLog(#"Not sent to server yet");
if (![profileIDCacheWaitingToRequest containsObject:profileID]) {
CXDebugLog(#"Not added to queue yet yet");
[filteredProfileIDCache addObject:[profileID copy]];
} else {
CXDebugLog(#"Already present in the queue, will fire soon");
}
} else {
CXDebugLog(#"Skipping profile download call for %#", profileID);
}
}
if (filteredProfileIDCache.count == 0) {
CXDebugLog(#"No need for submitting profile to server as we are already waiting on server to return..");
completion (nil, nil);
return;
}
for (NSString *pid in filteredProfileIDCache) {
if (profileIDCacheWaitingToRequest.count <= GROUP_PROFILE_MAX_COUNT) {
[profileIDCacheWaitingToRequest addObject:pid];
} else {
CXDebugLog(#"wait next turn. hope the user makes this call.");
continue;
}
}
CXProdLog(#"Queuing profiles for download...");
dispatch_async(_profile_batch_process_queue, ^{
// this block will keep executing once its a serial queue..
/////// SLEEP this thread, and wait for 0.6 seconds
sleep(0.6);
dispatch_async(dispatch_get_main_queue(), ^{
if (profileIDCacheWaitingToRequest.count == 0) {
completion (nil, nil);
return ;
}
NSArray *profileIDsToRequest = [profileIDCacheWaitingToRequest copy];
[profileIDArraySubmittedToTheServer addObjectsFromArray:profileIDsToRequest];
CXProdLog(#"Fetching profiles from server with count %lu", (unsigned long)profileIDsToRequest.count);
if (profileIDsToRequest.count > GROUP_PROFILE_MAX_COUNT) {
CXDebugLog(#"We are exceeding the count here");
NSMutableArray *array = [NSMutableArray arrayWithArray:profileIDsToRequest];
NSUInteger totalNumberOfObjectsToRemove = profileIDsToRequest.count - GROUP_PROFILE_MAX_COUNT;
for (int i = 0; i < totalNumberOfObjectsToRemove; i ++) {
[array removeLastObject];
}
profileIDsToRequest = array;
}
[CXDownloader downloadProfileForUsers:profileIDsToRequest sessionKey:self.sessionKey completion:^(NSDictionary *profileData, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[profileIDArraySubmittedToTheServer removeAllObjects];
if (profileData) {
....
}
else {
....
}
});
}];
[profileIDCacheWaitingToRequest removeAllObjects];
});
});
I'm trying to use BatteryCenter and CommonUtilities private frameworks under iOS 9.1 with the help of nst's iOS Runtime Headers. It's for research purposes and won't make it to the AppStore.
Here are their respective codes:
- (void)batteryCenter {
NSBundle *bundle = [NSBundle bundleWithPath:#"/System/Library/PrivateFrameworks/BatteryCenter.framework"];
BOOL success = [bundle load];
if(success) {
Class BCBatteryDevice = NSClassFromString(#"BCBatteryDevice");
id si = [[BCBatteryDevice alloc] init];
NSLog(#"Charging: %#", [si valueForKey:#"charging"]);
}
}
- (void)commonUtilities {
NSBundle *bundle = [NSBundle bundleWithPath:#"/System/Library/PrivateFrameworks/CommonUtilities.framework"];
BOOL success = [bundle load];
if(success) {
Class CommonUtilities = NSClassFromString(#"CUTWiFiManager");
id si = [CommonUtilities valueForKey:#"sharedInstance"];
NSLog(#"Is Wi-Fi Enabled: %#", [si valueForKey:#"isWiFiEnabled"]);
NSLog(#"Wi-Fi Scaled RSSI: %#", [si valueForKey:#"wiFiScaledRSSI"]);
NSLog(#"Wi-Fi Scaled RSSI: %#", [si valueForKey:#"lastWiFiPowerInfo"]);
}
}
Although I get the classes back, all of their respected values are NULL which is weird since some must be true, e.g. I'm connected to Wi-Fi so isWiFiEnabled should be YES.
What exactly is missing that my code doesn't return whats expected? Does it need entitlement(s)? If so what exactly?
In Swift, I managed to get this working without the BatteryCenter headers. I'm still looking for a way to access the list of attached batteries without using BCBatteryDeviceController, but this is what I have working so far:
Swift 3:
guard case let batteryCenterHandle = dlopen("/System/Library/PrivateFrameworks/BatteryCenter.framework/BatteryCenter", RTLD_LAZY), batteryCenterHandle != nil else {
fatalError("BatteryCenter not found")
}
guard let batteryDeviceControllerClass = NSClassFromString("BCBatteryDeviceController") as? NSObjectProtocol else {
fatalError("BCBatteryDeviceController not found")
}
let instance = batteryDeviceControllerClass.perform(Selector(("sharedInstance"))).takeUnretainedValue()
if let devices = instance.value(forKey: "connectedDevices") as? [AnyObject] {
// You will have more than one battery in connectedDevices if your device is using a Smart Case
for battery in devices {
print(battery)
}
}
Swift 2.2:
guard case let batteryCenterHandle = dlopen("/System/Library/PrivateFrameworks/BatteryCenter.framework/BatteryCenter", RTLD_LAZY) where batteryCenterHandle != nil else {
fatalError("BatteryCenter not found")
}
guard let c = NSClassFromString("BCBatteryDeviceController") as? NSObjectProtocol else {
fatalError("BCBatteryDeviceController not found")
}
let instance = c.performSelector("sharedInstance").takeUnretainedValue()
if let devices = instance.valueForKey("connectedDevices") as? [AnyObject] {
// You will have more than one battery in connectedDevices if your device is using a Smart Case
for battery in devices {
print(battery)
}
}
This logs:
<BCBatteryDevice: 0x15764a3d0; vendor = Apple; productIdentifier = 0; parts = (null); matchIdentifier = (null); baseIdentifier = InternalBattery-0; name = iPhone; percentCharge = 63; lowBattery = NO; connected = YES; charging = YES; internal = YES; powerSource = YES; poweredSoureState = AC Power; transportType = 1 >
You need to first access the BCBatteryDeviceController, after success block is executed, through which you can get list of all the connected devices.
Here is the code for the same.
Class CommonUtilities = NSClassFromString(#"BCBatteryDeviceController");
id si = [CommonUtilities valueForKey:#"sharedInstance"];
BCBatteryDeviceController* objBCBatteryDeviceController = si;
NSLog(#"Connected devices: %#", objBCBatteryDeviceController.connectedDevices);
I'm a bit confused about how and when to use beginBackgroundTaskWithExpirationHandler.
Apple shows in their examples to use it in applicationDidEnterBackground delegate, to get more time to complete some important task, usually a network transaction.
When looking on my app, it seems like most of my network stuff is important, and when one is started I would like to complete it if the user pressed the home button.
So is it accepted/good practice to wrap every network transaction (and I'm not talking about downloading big chunk of data, it mostly some short xml) with beginBackgroundTaskWithExpirationHandler to be on the safe side?
If you want your network transaction to continue in the background, then you'll need to wrap it in a background task. It's also very important that you call endBackgroundTask when you're finished - otherwise the app will be killed after its allotted time has expired.
Mine tend look something like this:
- (void) doUpdate
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self beginBackgroundUpdateTask];
NSURLResponse * response = nil;
NSError * error = nil;
NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error];
// Do something with the result
[self endBackgroundUpdateTask];
});
}
- (void) beginBackgroundUpdateTask
{
self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
}
- (void) endBackgroundUpdateTask
{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}
I have a UIBackgroundTaskIdentifier property for each background task
Equivalent code in Swift
func doUpdate () {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = beginBackgroundUpdateTask()
var response: URLResponse?, error: NSError?, request: NSURLRequest?
let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
// Do something with the result
endBackgroundUpdateTask(taskID)
})
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(expirationHandler: ({}))
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}
The accepted answer is very helpful and should be fine in most cases, however two things bothered me about it:
As a number of people have noted, storing the task identifier as a property means that it can be overwritten if the method is called multiple times, leading to a task that will never be gracefully ended until forced to end by the OS at the time expiration.
This pattern requires a unique property for every call to beginBackgroundTaskWithExpirationHandler which seems cumbersome if you have a larger app with lots of network methods.
To solve these issues, I wrote a singleton that takes care of all the plumbing and tracks active tasks in a dictionary. No properties needed to keep track of task identifiers. Seems to work well. Usage is simplified to:
//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];
//do stuff
//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];
Optionally, if you want to provide a completion block that does something beyond ending the task (which is built in) you can call:
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
//do stuff
}];
Relevant source code available below (singleton stuff excluded for brevity). Comments/feedback welcome.
- (id)init
{
self = [super init];
if (self) {
[self setTaskKeyCounter:0];
[self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
[self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];
}
return self;
}
- (NSUInteger)beginTask
{
return [self beginTaskWithCompletionHandler:nil];
}
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
//read the counter and increment it
NSUInteger taskKey;
#synchronized(self) {
taskKey = self.taskKeyCounter;
self.taskKeyCounter++;
}
//tell the OS to start a task that should continue in the background if needed
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endTaskWithKey:taskKey];
}];
//add this task identifier to the active task dictionary
[self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//store the completion block (if any)
if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//return the dictionary key
return taskKey;
}
- (void)endTaskWithKey:(NSUInteger)_key
{
#synchronized(self.dictTaskCompletionBlocks) {
//see if this task has a completion block
CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (completion) {
//run the completion block and remove it from the completion block dictionary
completion();
[self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
#synchronized(self.dictTaskIdentifiers) {
//see if this task has been ended yet
NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (taskId) {
//end the task and remove it from the active task dictionary
[[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
[self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
}
Here is a Swift class that encapsulates running a background task:
class BackgroundTask {
private let application: UIApplication
private var identifier = UIBackgroundTaskInvalid
init(application: UIApplication) {
self.application = application
}
class func run(application: UIApplication, handler: (BackgroundTask) -> ()) {
// NOTE: The handler must call end() when it is done
let backgroundTask = BackgroundTask(application: application)
backgroundTask.begin()
handler(backgroundTask)
}
func begin() {
self.identifier = application.beginBackgroundTaskWithExpirationHandler {
self.end()
}
}
func end() {
if (identifier != UIBackgroundTaskInvalid) {
application.endBackgroundTask(identifier)
}
identifier = UIBackgroundTaskInvalid
}
}
The simplest way to use it:
BackgroundTask.run(application) { backgroundTask in
// Do something
backgroundTask.end()
}
If you need to wait for a delegate callback before you end, then use something like this:
class MyClass {
backgroundTask: BackgroundTask?
func doSomething() {
backgroundTask = BackgroundTask(application)
backgroundTask!.begin()
// Do something that waits for callback
}
func callback() {
backgroundTask?.end()
backgroundTask = nil
}
}
As noted here and in answers to other SO questions, you do NOT want to use beginBackgroundTask only just when your app will go into the background; on the contrary, you should use a background task for any time-consuming operation whose completion you want to ensure even if the app does go into the background.
Therefore your code is likely to end up peppered with repetitions of the same boilerplate code for calling beginBackgroundTask and endBackgroundTask coherently. To prevent this repetition, it is certainly reasonable to want to package up the boilerplate into some single encapsulated entity.
I like some of the existing answers for doing that, but I think the best way is to use an Operation subclass:
You can enqueue the Operation onto any OperationQueue and manipulate that queue as you see fit. For example, you are free to cancel prematurely any existing operations on the queue.
If you have more than one thing to do, you can chain multiple background task Operations. Operations support dependencies.
The Operation Queue can (and should) be a background queue; thus, there is no need to worry about performing asynchronous code inside your task, because the Operation is the asynchronous code. (Indeed, it makes no sense to execute another level of asynchronous code inside an Operation, as the Operation would finish before that code could even start. If you needed to do that, you'd use another Operation.)
Here's a possible Operation subclass:
class BackgroundTaskOperation: Operation {
var whatToDo : (() -> ())?
var cleanup : (() -> ())?
override func main() {
guard !self.isCancelled else { return }
guard let whatToDo = self.whatToDo else { return }
var bti : UIBackgroundTaskIdentifier = .invalid
bti = UIApplication.shared.beginBackgroundTask {
self.cleanup?()
self.cancel()
UIApplication.shared.endBackgroundTask(bti) // cancellation
}
guard bti != .invalid else { return }
whatToDo()
guard !self.isCancelled else { return }
UIApplication.shared.endBackgroundTask(bti) // completion
}
}
It should be obvious how to use this, but in case it isn't, imagine we have a global OperationQueue:
let backgroundTaskQueue : OperationQueue = {
let q = OperationQueue()
q.maxConcurrentOperationCount = 1
return q
}()
So for a typical time-consuming batch of code we would say:
let task = BackgroundTaskOperation()
task.whatToDo = {
// do something here
}
backgroundTaskQueue.addOperation(task)
If your time-consuming batch of code can be divided into stages, you might want to bow out early if your task is cancelled. In that case, just return prematurely from the closure. Note that your reference to the task from within the closure needs to be weak or you'll get a retain cycle. Here's an artificial illustration:
let task = BackgroundTaskOperation()
task.whatToDo = { [weak task] in
guard let task = task else {return}
for i in 1...10000 {
guard !task.isCancelled else {return}
for j in 1...150000 {
let k = i*j
}
}
}
backgroundTaskQueue.addOperation(task)
In case you have cleanup to do in case the background task itself is cancelled prematurely, I've provided an optional cleanup handler property (not used in the preceding examples). Some other answers were criticised for not including that.
I implemented Joel's solution. Here is the complete code:
.h file:
#import <Foundation/Foundation.h>
#interface VMKBackgroundTaskManager : NSObject
+ (id) sharedTasks;
- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;
#end
.m file:
#import "VMKBackgroundTaskManager.h"
#interface VMKBackgroundTaskManager()
#property NSUInteger taskKeyCounter;
#property NSMutableDictionary *dictTaskIdentifiers;
#property NSMutableDictionary *dictTaskCompletionBlocks;
#end
#implementation VMKBackgroundTaskManager
+ (id)sharedTasks {
static VMKBackgroundTaskManager *sharedTasks = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedTasks = [[self alloc] init];
});
return sharedTasks;
}
- (id)init
{
self = [super init];
if (self) {
[self setTaskKeyCounter:0];
[self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
[self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];
}
return self;
}
- (NSUInteger)beginTask
{
return [self beginTaskWithCompletionHandler:nil];
}
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
//read the counter and increment it
NSUInteger taskKey;
#synchronized(self) {
taskKey = self.taskKeyCounter;
self.taskKeyCounter++;
}
//tell the OS to start a task that should continue in the background if needed
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endTaskWithKey:taskKey];
}];
//add this task identifier to the active task dictionary
[self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//store the completion block (if any)
if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//return the dictionary key
return taskKey;
}
- (void)endTaskWithKey:(NSUInteger)_key
{
#synchronized(self.dictTaskCompletionBlocks) {
//see if this task has a completion block
CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (completion) {
//run the completion block and remove it from the completion block dictionary
completion();
[self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
#synchronized(self.dictTaskIdentifiers) {
//see if this task has been ended yet
NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (taskId) {
//end the task and remove it from the active task dictionary
[[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
[self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
NSLog(#"Task ended");
}
}
}
#end