How to Prevent variables to be initialized twice? - ios

I am new to iOs app development and I wonder if my app architecture is correct. A bug in my app make me believe that some of my variables are initialized several times.
To summarize my app : 2 screens and 2 views but only one controller. Depending the current view, the viewDidLoad have different results.
I don't believe this is the right way to do. I guess the idea would be to create a controller for each view ?
But my main concern here : in my viewDidLoad, when the main screen is loaded, I set up a notification observer. I believe( because of bugs ) that this observer is setup each time the screen load and then is called multiple times.
My questions here : Where to put this listener , is there a place that will run the code only once this view is loaded ? It should be fixed by putting this listener into a variable ?
Is the AppDelegate application function a right place for that kind of things ?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Remove iphone sleep mode
UIApplication.shared.isIdleTimerDisabled = true
//Setup the external scanner object
self.scanner.delegate = self
self.scanner.connect()
// Init the saved values
let defaults = UserDefaults.standard
// --------------- MAIN VIEW ---------------
if(mainView != nil){
// Add a notification receiver
// Will receive results ### request
NotificationCenter.default.addObserver(self, selector: #selector(mainTextNewLineNotification), name: Notification.Name(rawValue: "sendingToView"), object: nil)
// Layout setup
mainTextView.layer.cornerRadius = 6.0
[...]
}
// --------------- SETTINGS VIEW ---------------
if(settingsView != nil){
//Fill the field with saved values
inputHost.text = defaults.string(forKey: "hostname")
inputPort.text = String(defaults.integer(forKey: "port"))
if(defaults.string(forKey: "timeout") != nil){
inputTimeout.text = defaults.string(forKey: "timeout")
}
if(UserDefaults().string(forKey: "confirmSwitch") == "On"){
confirmSwitch.isOn = true
} else {
confirmSwitch.isOn = false
}
}
}

You don't need to care about removeObserver logic in your case (since you use simple subscription using -selector, not -block). From Apple doc:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc(deinit) method.
Each time when you initialize instance of UIViewController and if system loads its view viewDidLoad will be called. viewDidLoad is called once during UIViewController lifecycle. So your logic for subscription is correct.
I don't know your application entire logic so it is hard what is the reason of your bug.

Related

CFNotificationCenter repeating events statements

I have been working on an enterprise iOS/Swift (iOS 11.3, Xcode 9.3.1) app in which I want to be notified if the screen changes (goes blank or becomes active) and capture the events in a Realm databse. I am using the answer from tbaranes in detect screen unlock events in IOS Swift and it works, but I find added repeats as the screen goes blank and becomes active:
Initial Blank: a single event recorded
Initial Re-activiation: two events are recorded
Second Blank: two events are recorded
Second Re-act: three events are recorded
and this cycle of adding an additional event recording each cycle.
This must be something in the code (or missing from the code) that is causing an additive effect but I can’t find it. And, yes, the print statements show the issue is not within the Realm database, but are actual repeated statements.
My code is below. Any suggestions are appreciated.
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
Unmanaged.passUnretained(self).toOpaque(), // observer
displayStatusChangedCallback, // callback
"com.apple.springboard.hasBlankedScreen" as CFString, // event name
nil, // object
.deliverImmediately)
}
private let displayStatusChangedCallback: CFNotificationCallback = { _, cfObserver, cfName, _, _ in
guard let lockState = cfName?.rawValue as String? else {
return
}
let catcher = Unmanaged<AppDelegate>.fromOpaque(UnsafeRawPointer(OpaquePointer(cfObserver)!)).takeUnretainedValue()
catcher.displayStatusChanged(lockState)
print("how many times?")
}
private func displayStatusChanged(_ lockState: String) {
// the "com.apple.springboard.lockcomplete" notification will always come after the "com.apple.springboard.lockstate" notification
print("Darwin notification NAME = \(lockState)")
if lockState == "com.apple.springboard.hasBlankedScreen" {
print("A single Blank Screen")
let statusString = dbSource() // Realm database
statusString.infoString = "blanked screen"
print("statusString: \(statusString)")
statusString.save()
return
}

Inconsistent value from NSUserDefaults at app startup

Under the iOS simulator, I'm having trouble getting a consistent value back from NSUserDefault on app startup. I've set the value in a previous run of the app (and I put some prints in there to make sure there was no accidental changes), so I don't think there's a problem with the way I'm calling synchronize(). The reads are happening on the main thread also, and after I've set the default values. After a couple of seconds, if I retry the fetch from NSUserDefaults, I get the correct value (I put a timer in there to verify that the value gets corrected and seems to stay the same after startup).
The value represents where the user will store the data (on iCloud or just on the local device) and is represented by an integer in the user defaults. Here's the enum representing the values:
class MovingDocument: UIDocument {
enum StorageSettingsState: Int {
case NoChoice = 0
case Local = 1
case ICloud = 2
}
}
Here's an excerpt of the class used to access NSUserDefaults:
class Settings {
static let global = Settings()
private enum LocalStoreKeys : String {
case IsHorizontal = "IsHorizontal"
case ItemInPortrait = "ItemInPortrait"
case RotateTitle = "RotateTitle"
case StorageLocation = "StorageLocation"
}
// Other computed variables here for the other settings.
// Computed variable for the storage setting.
var storageLocation: MovingDocument.StorageSettingsState {
get {
print("Storage state fetch: \(NSUserDefaults.standardUserDefaults().integerForKey(LocalStoreKeys.StorageLocation.rawValue))")
return MovingDocument.StorageSettingsState(rawValue: NSUserDefaults.standardUserDefaults().integerForKey(LocalStoreKeys.StorageLocation.rawValue)) ?? .NoChoice
}
set {
print("Setting storage location to \(newValue) (\(newValue.rawValue))")
NSUserDefaults.standardUserDefaults().setInteger(newValue.rawValue, forKey: LocalStoreKeys.StorageLocation.rawValue)
synchronize()
CollectionDocument.settingsChanged()
}
}
func prepDefaultValues() {
NSUserDefaults.standardUserDefaults().registerDefaults([
LocalStoreKeys.IsHorizontal.rawValue: true,
LocalStoreKeys.ItemInPortrait.rawValue: true,
LocalStoreKeys.RotateTitle.rawValue: true,
LocalStoreKeys.StorageLocation.rawValue: MovingDocument.StorageSettingsState.NoChoice.rawValue
])
}
func synchronize() {
NSUserDefaults.standardUserDefaults().synchronize()
}
}
The value that I should be reading (and which I always seem to get after startup) is the value '1' or .Local. The value that I am sometimes getting instead is '0' or .NoChoice (which just happens to be the default value). So, it looks like it's using the default value until some later point in the application startup.
For completeness sake, here's the relevant app delegate code:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
Settings.global.prepDefaultValues() // Must be called before doing CollectionDocument.start() because we need the default value for storage location.
CollectionDocument.start()
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
browserController = ViewController()
navController = UINavigationController(rootViewController: browserController!)
navController!.navigationBar.translucent = false
self.window!.rootViewController = navController
self.window!.makeKeyAndVisible()
checkStorageLocation()
return true
}
The getter is called within CollectionDocument.start() (CollectionDocument is a subclass of MovingDocument, where the location enum is defined). The call at the end to checkStorageLocation() is my periodic debugging check for the value of the storage location (it calls itself back via dispatch_after()).
As a side note, I also tried putting most of the code in the application(didFinishLaunchingWithOptions...) into a dispatch_async call (on the main thread) to see if that made a difference (just the call to set up the default values and the return statement were done immediately, the rest of the calls were put into the asynchronous call block), and it still got the wrong value (sometimes). I guess I could try using dispatch_after instead (and wait a second or two), but I can't really start the app until this is complete (because I can't read the user data from the doc until I know where it is) and a loading screen for just this one problem seems ridiculous.
Any ideas about what might be going wrong? I'm unaware of any limitations on NSUserDefaults as far as when you are allowed to read from it...
Rebooting my mac fixed the problem (whatever it was). iOS 10, NSUserDefaults Does Not Work has a similar problem and the same solution (though the circumstances are quite different, since I have tried neither Xcode 8 nor iOS 10).

how to block ios swift when no network, any best practice?

I want to block my app's ui when there is no network connectivity.
How can I do this?
Creating a blocking wide-as-screen transparent view
move it to the front when needed to block ui touches?
move it to the rear when network is back?
Is there a best UX practice for this backed up in swift implementation?
Instead of doing all that you can just disable user interaction for that particular view. Like below
[self.view setUserInteractionEnabled:NO];
self.view.userInteractionEnabled = false //swift implementation
This will disable user interaction for all subviews of that view
If code that handles internet connection is not in currently shown view controller
UIApplication.sharedApplication().keyWindow.rootViewController.view.userInteractionEnabled = false
It's not ideal but I have solution based on Nanayakkara project. AppDelegate creates MyConnectionManager which is observed on networkStatusChanged selector:
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("networkStatusChanged:"), name: ReachabilityStatusChangedNotification, object: nil)
Reach().monitorReachabilityChanges()
Each time connection state was changed manager calls networkStatusChanged and checks if connection is lost & top view isn't special connection view with message like "Please check your internet connection". If it isn't manager retrieves topController from sharedApplication
func topController() -> UIViewController? {
if var topRootController =
UIApplication.sharedApplication().keyWindow?.rootViewController {
while((topRootController.presentedViewController) != nil) {
topRootController = topRootController.presentedViewController!
}
return topRootController
}
return nil
}
and calls presentViewController with ConnectionViewController.

Why does Example app using Photos framework use stopCachingImagesForAllAssets after each change?

So I'm looking at the Photos API and Apple's sample code here
https://developer.apple.com/library/ios/samplecode/UsingPhotosFramework/Introduction/Intro.html
and its conversion to swift here
https://github.com/ooper-shlab/SamplePhotosApp-Swift
I have integrated the code into my project so that a collectionView is successfully updating itself form the library as I take photos. There is one quirk: Sometimes cells are blank, and it seems to be connected to stopCachingImagesForAllAssets which Apple calls each time the library is updated at the end of photoLibraryDidChange delegate method.
I can remove the line and it fixes the problem, but surely there is a reason Apple put it there in the first place? I am concerned with memory usage.
// MARK: - PHPhotoLibraryChangeObserver
func photoLibraryDidChange(changeInstance: PHChange) {
// Check if there are changes to the assets we are showing.
guard let
assetsFetchResults = self.assetsFetchResults,
collectionChanges = changeInstance.changeDetailsForFetchResult(assetsFetchResults)
else {return}
/*
Change notifications may be made on a background queue. Re-dispatch to the
main queue before acting on the change as we'll be updating the UI.
*/
dispatch_async(dispatch_get_main_queue()) {
// Get the new fetch result.
self.assetsFetchResults = collectionChanges.fetchResultAfterChanges
let collectionView = self.pictureCollectionView!
if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
// Reload the collection view if the incremental diffs are not available
collectionView.reloadData()
} else {
/*
Tell the collection view to animate insertions and deletions if we
have incremental diffs.
*/
collectionView.performBatchUpdates({
if let removedIndexes = collectionChanges.removedIndexes
where removedIndexes.count > 0 {
collectionView.deleteItemsAtIndexPaths(removedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let insertedIndexes = collectionChanges.insertedIndexes
where insertedIndexes.count > 0 {
collectionView.insertItemsAtIndexPaths(insertedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
if let changedIndexes = collectionChanges.changedIndexes
where changedIndexes.count > 0 {
collectionView.reloadItemsAtIndexPaths(changedIndexes.aapl_indexPathsFromIndexesWithSection(0))
}
}, completion: nil)
}
self.resetCachedAssets() //perhaps prevents memory warning but causes the empty cells
}
}
//MARK: - Asset Caching
private func resetCachedAssets() {
self.imageManager?.stopCachingImagesForAllAssets()
self.previousPreheatRect = CGRectZero
}
I was having the same result.
Here's what fixed the issue for me:
Since performBatchUpdates is asynchronous, the resetCachedAssets gets executed possibly while the delete/insert/reload is happening, or even between those.
That didn't sound nice to me. So I moved the line:
self.resetCachedAssets()
to the first line of the dispatch_async block.
I hope this helps you too.

WatchKit InterfaceController vs ViewController

Are awakeWithContext, willActivate, didDeactivate the same as viewDidLoad, viewWillAppear, viewDidAppear in terms of functionality?
I am porting code from a Swift Apple Watch tutorial that was created back when people had to add their own watch AppViewController file to test their watch apps.
The included files and things have changed with the official watch release of Xcode obviously so I’m wondering where to put where.
For example there is some code in the older AppViewController file and so I just copy/pasted it into the new InterfaceController. I put code that was in viewDidLoad, viewWillAppear, viewDidAppear into awakeWithContext, willActivate, didDeactivate respectively.
It seems the methods are different. I got 1 error saying that setText doesn’t exist:
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
…and 2 errors saying view doesn’t exist:
iconLabel.frame = self.view.bounds
self.view.insertSubview(iconLabel, atIndex: 1)
It’s like WatchKit doesn’t use some of the normal property methods or something.
Error Messages:
http://i.imgur.com/wXMdt3c.png
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
self.view.insertSubview(iconLabel, atIndex: 1) // Xcode error
}
override func willActivate() {
super.willActivate()
iconLabel.frame = self.view.bounds // Xcode error
iconLabel.textAlignment = .Center
iconLabel.font = UIFont.boldSystemFontOfSize(132)
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
newBeat()
NSTimer.scheduledTimerWithTimeInterval(8,
target: self,
selector: Selector("newBeat"),
userInfo: nil,
repeats: true)
beat()
}
func newBeat() {
// 1
if ++currentBeatPatternIndex == beatPatterns.count {
currentBeatPatternIndex = 0
}
// 2
currentBeatPattern = beatPatterns[currentBeatPatternIndex]
// 3
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)" // Xcode error
iconLabel.text = currentBeatPattern.icon
}
func beat() {
// 1
UIView.animateWithDuration(currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 2
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.shrinkFactor, self.shrinkFactor)
},
completion: { _ in
// 3
UIView.animateWithDuration(self.currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 4
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.expandFactor, self.expandFactor)
},
completion: { _ in
// 5
self.beat()
}
)
}
)
}
}
You are right that awakeWithContext, willActivate, and didDeactivate are very similar to the existing UIViewController methods like viewDidLoad, viewWillAppear, and viewDidUnload. The errors you're seeing however are related to the way WatchKit currently works. In order for a watch app to run, all the code is executed on an iPhone but the Apple Watch itself assumes responsibility for the UI elements. What that means is that any views that constitute your watch app MUST be included on your watch app's storyboard. Also, as a direct result, views cannot be instantiated and added to a parent view. All UI elements must be included in your watch app's storyboard and the watch will lay them out based on their arrangement in interface builder. This means you cannot call addSubview and you cannot set an element's frame. You can only adjust its size and set its hidden property to hide or show it on the watch. As far as this method goes –
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
You are calling the method wrong. In swift the parameters are included in the parentheses. You can call it this way if it's what you mean
bpmLabel.setText("\(currentBeatPattern.bpm)")
but setText is a method that takes a string parameter and cannot be assigned with =
As far as the animations go, I think you're out of luck. Watch apps currently are more like widgets than iOS apps and things like UIView animations and frame math are not available you. You should definitely read
up on WatchKit because there's no way you'll be able to port an iOS app directly to the watch like this.

Resources