Reputation: 31
I'm working on a project where I have an app that receives data over BLE from a wearable peripheral, but I'm struggling with how to architect the app. Currently I have a singleton BLEManager class that constantly receives data in the background and then uses NSNotificationCenter to send it to the active view controller. This works but has gotten messy and seems non-ideal since I have multiple view controllers that each process the data in the same way and then just display it differently. Additionally, there are some settings related to the data processing that can be changed in app and need to be the same everywhere. It would be nice if the BLEManager sent the data to a central processing class and then the processed data was sent to the active view controller but I'm not sure the best way to set this up.
I could incorporate all the processing into the BLEManager class but then it would get pretty bloated and unintuitive and wouldn't be nice to work with moving forward. If I make a separate processing class thats a property of the BLEManager then I'd have to go through the BLEManager if I wanted to get or change any variables in the processing class from anywhere else which would be annoying. I could make a singleton processing class that receives data from the BLEManager and then sends it to the active VC but I've seen people say to avoid singletons so I'm hesitant to use another one even though this seems like it could be a good solution.
Is there a standard or recommended way to architect an iOS app to process incoming data from Bluetooth and then send it wherever its needed?
Upvotes: 3
Views: 818
Reputation: 16774
In cases where you create a large system for communicating with the BLE (others as well) the BLEManager
should not be a singleton at all. This needs to be an object that is capable of handling all the bluetooth communication (and I am sure it already does) and report the relevant data through delegation.
What you need at this point is an interface for this object which in your case would best be a singleton. This class should have a minimized interface to what you need for managing these data and should not expose the BLEManager
at all. That means creating all the methods such as initialization of the BLEManager
which make sense from the usage perspective. In some cases that will be copying the part of the interface you already did in the BLEManager
and some methods will most likely simply forward the message to the manager.
This interface class may seem a bit useless at the beginning but in the end it will save your life. What you should put in this class are all other possible attachments such as a local database manager, an API manager, analytics, mocking...
So for your specific case it would seem your task list is:
BLEManager
class which is capable of communicating through the bluetooth.BLEManager
BLEManager
and the data manager. The singleton should provide a public interface that makes sense from the usage perspective and include the delegation system or the notification system.The interface should then look something like this:
Header
@protocol CoreManagerDelegate
- (void)coreManager:(CoreManager *)sender updatedData:(id)data;
@optional
- (void)coreManager:(CoreManager *)sender encounteredIssue:(NSError *)issue;
- (void)coreManager:(CoreManager *)sender changedConnectionState:(BluetoothConnectionState)isConnected; // BluetoothConnectionStateUnkown, BluetoothConnectionStateDisabled, BluetoothConnectionStateNotPaired, BluetoothConnectionStateConnected...
@end
@interface CoreManager : Singleton
@property (weak) id<CoreManagerDelegate> delegate;
- (void)initialize;
- (void)fetchBluetoothData;
- (id)getCurrentData;
@end
Source
@interface CoreManager()
@property BLEManager *bluetoothManager;
@property DataManager *dataManager;
@end
@implementation CoreManager
#pragma mark - Bluetooth manager attachment
- (void)fetchBluetoothData {
[self.bluetoothManager fetchSomeData];
}
#pragma mark Delegate
- (void)BLEManager:(BLEManager *)sender receivedNewData:(id)data {
// do data validation, log if needed, report...
[self.dataManager insertNewData:data];
}
- (void)BLEManager:(BLEManager *)sender encounteredIssue:(NSError *)issue {
// modify and evaluate the error if needed
if([self.delegate respondsToSelector:@selector(coreManager:encounteredIssue:)]) {
[self.delegate coreManager:self encounteredIssue:issue];
}
else {
// log error ?
}
// send the error to a remote service
}
#pragma mark - Data manager attachment
- (id)getCurrentData {
return [self.dataManager currentData];
}
#pragma mark Delegate
- (void)dataManager:(DataManager *)sender updatedData:(id)processedData {
[self.delegate coreManager:self updatedData:processedData];
}
- (void)dataManager:(BLEManager *)sender encounteredIssue:(NSError *)issue {
// modify and evaluate the error if needed
if([self.delegate respondsToSelector:@selector(coreManager:encounteredIssue:)]) {
[self.delegate coreManager:self encounteredIssue:issue];
}
else {
// log error ?
}
// send the error to a remote service
}
@end
This will now maintain a very clean and agile code. It will prevent any unnecessary class or file inflation. It presents a very nice opportunity for testing and mocking data. Adding additional features will be transparent from the usage perspective: For instance implementing core data will produce no changes in the user interface part of your project.
The id
types should be replaced by concrete custom objects if possible. Even the errors and logs are best being custom so you may easily track them across the application, enable and disable them at will.
Note: If you think it is too large of a change to transform your bluetooth manager to a normal object instead of singleton at this point you may still keep it a singleton and simply override the getter for it to return the shared instance.
I am sure there are many other good practices but this is the one I use mostly and has proven to be the best so far. I hope it will help you.
Upvotes: 1
Reputation: 1871
I don't know of a standard design pattern for this but I have seen the Singleton BLEManager used extensively. My understanding is that you only want one object dealing with the BLE channel.
To keep your BLEManager from getting too bloated you can use the Delegation pattern with a different protocol per BLE message you need to handle. This way your BLEManager is just routing messages through delegates which handle each type of message.
This will give you a better separation of concerns so that your BLEManager does not get bloated. The BLEManager will then be a dumb pass through with the real work happening in the message handlers delegates.
Upvotes: 0