I am using Xcode 8 and swift 2.3
I want to save the entire view controller to file and restore state even after app closes.
I searched everywhere and found we need to use coder for that. but all just shows to save an object.
but here I need to save entire ViewContoller and subviews.
ViewCotroller will have three buttons
Add Text
Add Image : User can add any number of textViews and Images. So I need to save all that info also.
Add ViewController : User may have an array of this viewController and need to save all.
Question 1)
Can just save self.view and can it save all subviews automatically ?
Question 2)
I need to init without coder at start by just using
let nameVccVar = nameVcc()
and
let nameVccVar = nameVcc(coder: CodeVar)
Question 3)
How do I save all this coded data to file using NSKeyedUnarchiver and retrieve back?
Kindly help me or give me tips to make all this work
class nameVcc: UIViewController
{
var nameIntVar = 0
var nameStringVar = "Save this"
var nameImageVar = UIImage()
override func viewDidLoad()
{
super.viewDidLoad()
}
required init?(coder NkdPsgVar: NSCoder)
{
super.init(coder: NkdPsgVar)
}
override func encodeWithCoder(DkdPsgVar: NSCoder)
{
}
func addTextViewBtnClick()
{
let viewVar = UIView()
// Set many values for view
self.view.addSubview(viewVar)
}
func addImageViewBtnClick()
{
let imgViewVar = UIImageView()
// Set many values for ImageView
self.view.addSubview(imgViewVar)
}
}
I also tired :
convenience init()
{
self.init()
}
and
convenience init()
{
self.init(coder: NSCoder())
}
TL;DR:
There are key steps and methods required to implement state restoration in your app.
Opting-in for App State Restoration in App Delegate by returning true for these methods shouldSaveApplicationState and shouldRestoreApplicationState.
Setting Restoration Identifier for view controllers that you want state restoration implemented in.
Implementing encodeRestorableStateWithCoder and decodeRestorableStateWithCoder methods in your view controller. The former is used to save any state information of your view controller to disk using encodeObjectForKey method. The latter shall be used to restore the state back from your saved contents to the disk by using decodeObjectForKey method.
Watch this awesome blog post about State Restoration for easier grasp. If you have time, also do spend on watching State Restoration WWDC Session.
There is no need to save your entire ViewController yourself. UIKit does that for you when you set your Restoration Identifier (In Interface Builder). The only thing we need to focus for state restoration is to save your essential properties needed to re-create your app's "State", for example, a Bool property which determines whether you want to display a specific button or not.
Now coming to your series of questions....
Can I just save self.view and can it save all subviews automatically ?
You do not need to save any of your views. All subviews will be handled by UIKit. Encode your required properties (such as Bool flags, count variables, key properties that can be used to fetch data from api call for your datasource etc.) inside encodeRestorableStateWithCoder method. Don't forget to re-contruct your view controller's state from decodeRestorableStateWithCoder. Both of these methods belond to UIStateRestoring protocol.
I need to init without coder at start by just using
No need to do any fancy inits.
How do I save all this coded data to file using NSKeyedUnarchiver and
retrieve back?
As I said earlier, implement necessary UIStateRestoring protocol methods to save and restore your app's state.
Related
I have a View-Hierarchy like this:
UIViewController (SingleEventViewController)
UIScrollView (EventScrollView)
UIView (contentView)
3xUITableView (SurePeopleTV, MaybePeopleTV, NopePeopleTV (all inherited from the same UITableView)), & all other UI-Elements
The SingleEventViewController stores one Event (passed within the initializer). (All Events are stored in Core-Data).
The three UITableViews are there for displaying the users which are participating (or not or maybe) at the Event. My question is, what are the possibilities to fill the tableViews with the data and what would you recommend in which situation.
Currently I have a property parentVC: SingleEventViewController in all Subviews and get the data like this:
override func loadUsers() {
//class SurePeopleTV
guard let parentController = parentVC else { return }
users = (parentController.thisEvent.eventSureParticipants?.allObjects as! [User])
finishedLoading = true
super.loadUsers()
}
.
func applyDefaultValues() {
//class EventScrollView
guard let parent = parentVC else { return }
titleLabel.text = parent.eventName
}
I'm new to programming but I got a feeling that I should not create a parentVC reference in all of my classes.
An object should not (ideally) know about its parent - if it does they are "tightly coupled". If you change the object's parent, your code may break. In your case, your parent object must have a thisEvent property.
You want your objects to be "loosely coupled", so the object doesn't know about a specific parent object.
In Swift, the usual ways to pass information "back up the chain" is to use the delegate design pattern… https://developer.apple.com/documentation/swift/cocoa_design_patterns or to use closures.
See also https://www.andrewcbancroft.com/2015/04/08/how-delegation-works-a-swift-developer-guide/ for info on delegation
First of all, if you create a reference to the parent ViewController make sure it is weak, otherwise you can run into memory management issues.
Edit: As Ashley Mills said, delegates the way to handle this
The recommended way to pass data between ViewControllers is using something like this
Every time a segue is performed from the view controller this function is in this function is called. This code first checks what identifier the segue has, and if it is the one that you want, you can access a reference to the next view controller and pass data to it.
this Main Menu VC will be opened when the app launched for the first time or after the user back to the app (the app become active after enter the background state).
every time this main menu VC is opened, ideally I need to update the time that the date time data comes from the server. in this main menu vc class I call getDateTimeFromServer() after that I updateUI().
but to update the data after the app enter the background and back to the foreground, the getDateTimeFromServer() and updateUI() shall be activated from Appdelegate using function.
func applicationWillEnterForeground(application: UIApplication) {
}
so how do I activate a method that are exist in Main Menu VC from AppDelegate
You don’t need to call the view controller method in app delegate. Observe foreground event in your controller and call your method from there itself.
Observe for the UIApplicationWillEnterForeground notification in your viewController viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.yourMethod), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
Implement this to receive callback when user enters foreground
#objc func yourMethod() {
// Call getDateTimeFromServer()
}
These types of messaging are in most cases done with static context. As it was already mentioned you could alternatively use notification center within the within the view controller to be notified of your application entering foreground. I discourage you creating custom notifications for this though (but is a possible solution as well).
Anyway for your specific case I suggest you have a model that contains your data. Then create a shared instance of it.
class MyDataModel {
static var shared: MyDataModel = {
let model = MyDataModel()
model.reloadData()
return model
}()
var myObjects: [MyObject]?
func reloadData() {
// load data asynchronously
}
}
Now when your view controller needs to reload it simply uses MyDataModel.shared.myObjects as data source.
In app delegate all you do is reload it when app comes back to foreground using MyDataModel.shared.reloadData().
So now a delegate is still missing so we add
protocol MyDataModelDelegate: class {
func myDataModel(_ sender: MyDataModel, updatedObjects objects: [MyObject]?)
}
class MyDataModel {
weak var delegate: MyDataModelDelegate?
static var shared: MyDataModel = {
Now when your view controller appears it needs to assign itself as a delegate MyDataModel.shared.delegate = self. And implement the protocol in which a reload on the view must be made.
A callout to the delegate can simply be done in a model setter:
}()
var myObjects: [MyObject]? {
didSet {
delegate.myDataModel(self, updatedObjects: myObjects)
}
}
func reloadData() {
You can do something like that, using a technique called Key-Value Observation:
class CommonObservableData: NSObject {
// Use #objc and dynamic to ensure enabling Key-Value Observation
#objc dynamic var dateTime: Date?
static let shared = CommonObservableData()
func updateFromWeb() {
// callWebThen is a function you will define that calls your Web API, then
// calls a completion handler you define, passing new value to your handler
callWeb(then: { self.dateTime = $0 })
}
}
Then you observe on it using Swift 4 's new NSKeyValueObservation.
class SomeViewController: UIViewController {
var kvo: NSKeyValueObservation?
func viewDidLoad() {
...
kvo = CommonObservableData.shared.observe(
\CommonObservableData.dateTime, { model, change in
self.label.text = "\(model.dateTime)"
})
}
}
Key-Value Observation is originally an Objective-C technique that is "somewhat revived" by Swift 4, this technique allows you to observe changes on a property (called a Key in Objective-C) of any object.
So, in the previous code snippets, we made a class, and made it a singleton, this singleton has an observable property called dateTime, where we could observe on change of this property, and make any change in this property automatically calls a method where we could update the UI.
Read about KVO here:
Key-Value Observation Apple Programming Guide
Key-Value Observation using Swift 4
Also, if you like Rx and RFP (Reactive Functional Programming), you can use RxSwift and do the observation in a cleaner way using it.
In swift 4 and 5, the notification name is changed the below code working for both.
notifyCenter.addObserver(self, selector: #selector(new), name:UIApplication.willEnterForegroundNotification, object: nil)
#objc func new(){}
In all iOS classes that use Firebase you will have code like this,
private func clearObservations() {
// your method for clearing observations, probably something like
blah blah. removeAllObservers()
}
In view controllers, it's essential that you call this in viewWillDisappear (or viewDidDisappear)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
clearObservations()
}
That's fine.
Assume that you have created an observation in a UITableViewCell.
What is the best place in a cell to "clear observations" ?
Note that prepareForReuse is useless, try it.
The only approach we've found is
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
clearObservations()
}
super.willMove(toSuperview: newSuperview)
}
Seems flakey/bizarre though.
What's the deal on this?
Update
Note while "XY Answers" are interesting and informative, if anyone knows the answer to the question that would be great also!
Preface
This was an attempt to answer the question but the question was misunderstood. I'll leave it here as it does have some relevance regarding observers, handles and tableView cell interaction.
While you can go through those gyrations, it's not really needed in most use cases.
For example, if you add and observer to a node, there wouldn't necessarily be a someRef? variable hanging around. So here we are watching the Posts node for new posts
let postsRef = self.ref.child("Posts")
postsRef.observe(.childAdded, with: { snapshot in
print(snapshot) //add the post to the dataSource and reloadTableview/cell
})
Here's another example of watching for any posts that are changed by uid_2
let postsRef = self.ref.child("Posts")
let queryRef = postsRef.queryOrdered(byChild: "poster_id").queryEqual(toValue: "uid_2")
queryRef.observe(.childChanged) { (snapshot) in
print(snapshot) //change the post in the dataSource and reloadTableview/cell
}
No class vars are needed for this functionality and nothing needs be nil'd. The point here being that you do not have to have class vars to get observing functionality and you do not need to keep a handle for every observer (keep reading)
In view controllers, it's essential that you call this
(someRef?.removeAllObservers()) in viewWillDisappear (or Did)..
will use Firebase in the cells of tables.
To clarify; I wouldn't want to put Firebase observers in the cells of tables. The observers should be in whichever viewController controls the tableView that has cells. Cells should pull data from the dataSource array (which is backed by Firebase)
There are some circumstances where you may want to remove all observers, again no need to have a class var or a need to nil a var.
let postsRef = self.ref.child("Posts")
postsRef.removeAllObservers()
There are times when a specific observer needs to be removed (in the case where a node has observers on it's child nodes for example), and in those cases, we store a handle to that observer as say, a class var (keeping them in an array is a tidy way to do it)
class ViewController: UIViewController {
var myPostHandle : DatabaseHandle?
func addObserver() {
let postsRef = self.ref.child("Posts")
self.myPostHandle = postsRef.observe(.childAdded, with: { snapshot in
print(snapshot)
})
func stopObserving() {
if self.myPostHandle != nil {
let postsRef = self.ref.child("Posts")
postsRef.removeObserver(withHandle: self.myPostHandle) //remove only the .childAdded observer
}
}
}
Again though, once the observer is removed, the handle would go out of scope once the class closes.
Tableviews that contain cells are backed by a dataSource and that dataSource get's it's data from firebase. When something is added, changed or removed from Firebase, your app is notified and the array is updated and then the cell refreshed. No need for an observer in the cell itself.
There's no need to add dozens of observers (in the cells) - add one central observer and let it keep the array current. Refresh tableView only when something changes.
EDIT
To Address a comment regarding the use of removeAllObservers: code is worth 1000 words:
Create a new Firebase project with two button actions. Here's the code for button0 which adds an observer to a node:
func button0() {
let testRef = self.ref.child("test_node")
testRef.observe( .value) { snapshot in
print(snapshot)
}
}
when this button0 is clicked, from there on, any adds, changes, or deletes to the test node will print it's contents to the log.
func button1() {
let testRef = self.ref.child("test_node")
testRef.removeAllObservers()
}
This will remove all observers for the node specified. Once clicked, no events will print to the console.
Try it!
It is not right to clear observations in cell and therefore there is not a best place to do it in cell, because, firstly, this approach contradicts MVC pattern. Views only responsible for displaying content and they should only contain code that describes how they must be draw. And in the view controller you give the content for showing by views. Usually content has provided by your model. So controller connects views and model. In your case, when you place clearObservations() in cell class, you also have someRef as a class property, so you have a model in your view class and this is incorrect.
Secondly, if you try to clear observations in table cell you definitely make logic of showing some content in table in wrong way. Cell only show data that has to be generated by some object that conforms to UITableViewDataSource protocol and implements protocol methods. For instance, in cellForRow method you generate cell and setup it with some content from array. This array is generated from model (Firebase service). Your view controller may be this data source object. You have to include array property to controller class and someRef, than you fill array and reload table data. If controller's view disappeared you clear observations, but you do it only inside view controller (in viewWillDisappear()).
Overall, all manipulations with someRef you should do in view controller and therefore "clear observations" also inside controller.
I am working on an open source tutorial using MVVM, Coordinators and RxSwift. I am constructing all the viewcontrollers and models in the coordinator. Controller has a strong reference to viewmodel and when a viewmodel is set, I would like to perform some UI related actions(using property observer didSet). The problem I am facing is that didSet is called before viewDidLoad causing a crash.
Stripped down version of ViewController:
class MessageVC: UIViewController {
var viewModel: MessageViewModel! {
didSet {
manipulateUI() // crashes
}
}
override func viewDidLoad() {
super.viewDidLoad()
manipulateUI() // works fine if setup is correct in coordinator
}
Coordinator stripped down version:
extension AppCoordinator {
convenience init() {
let rootVC = MessageVC() // actual construction from storyboard
let messages = Message.getMessages()
rootVC.viewModel = MessageViewModel(withMessage: messages)
}
My concern is that even though calling manipulateUI in viewDidLoad is working for me currently, the app will crash if I forget to set the viewModel from my co-ordinator making me think that I am using a fragile architecture. I really like updating userinterface from didSet but it is called before viewDidLoad.
I know it is a simple problem but from architecture standpoint it seems fragile. Any suggestions, improvements and comments are appreciated a lot.
I wont say that cases like this can define wether you are dealing with fragile architecture or not because view controllers has their own life cycle which differs a lot from other objects life cycle. Anyway you can easily avoid crashes here using different approaches. For example :
Approach 1:
Put a guard statement at the very beginning of your manipulateUI function so this function wont manipulate UI until both view is loaded and model is set. Then call this function on viewDidLoad method and when viewModel is set:
func manipulateUI(){
guard let viewModel = self.viewModel , isViewLoaded else {
return
}
//continue manipulation here
}
Approach 2:
Since you are not sure wether view is loaded when you set the model and don't know if views are initialized yet, you can access the views as optional properties in manipulateUI function:
func manipulateUI(){
self.someLabel?.text = self.viewModel.someText
//continue manipulation here
}
Approach 3:
Since you are using RxSwift you can always register an observer for view controller's isViewLoaded property and set the data source after you are sure that view is loaded
Crash happens because at this point
rootVC.viewModel = MessageViewModel(withMessage: messages)
view controller is not initialized.
It won't work the way you're trying to accomplish, you have to call manipulateUI() inside viewDidLoad.
I have an already setup view and want to wrap it in a subclass of UIView.
class ElementView: UIView {
var selected = false
}
The problem is that I cannot initialize ElementView with the already existing view.
Swift doesn't allow assigning to self too.
init(view: UIView) {
//here I would have to call super.init(coder: NSCoder?) or super.init(frame: CGRect)
//none of which actually properly initializes the object
}
Any ideas how to implement this?
Thanks :)
Clarification:
I will give you the larger context hoping it'd be more clear:
I am implementing a UIScrollView subclass. The scroll view contains an array of UIView objects which are added externally (from the user of the class).
In my custom UIScrollView class I want to implement a tap gesture recognizer for each object. That's already done:
let singleTap = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
singleTap.cancelsTouchesInView = false
addGestureRecognizer(singleTap)
and the handler:
func handleTap(recognizer: UITapGestureRecognizer) {
//some code to handle the tap
}
The problem arises when I want to handle the tap, I want to store the previous state of the view (was it tapped before or not) in order to toggle that state when the tap happens. I want to do different stuff to the UIView depending on its state.
recognizer.view returns the view to which the recognizer is attached to, which is what I need. But, this way I have no possibility of implementing a state for the UIView.
That's why I wanted to implement a custom wrapper for UIView which should contain the state information (which is also a problem). That's how I came up to asking this question...
In order to create a custom init for the UIView subclass you have to call the required init for the UIView superclass. Also while creating the custom init you have to send the Frame of the view to the superclass. Upon fulfilling these requirements you are free to pass on any arguments to the newly created init including the tap recognizer info.
Remember, if you are creating any variables - they have to be not nil upon the creation of the instance, thus in variable declaration you have to create some initial argument (for example - 0 for Int, etc.) for the initializer to work. Here is the example code:
var variableOne: Int = 0
var variableTwo: Int = 0
init(variableOne: Int, variableTwo: Int) {
self.variableOne = variableOne
self.variableTwo = variableTwo
super.init(frame: CGRectZero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
It sounds like you are trying to mimic a copy constructor, and it sounds like you are trying to build it in IB. The short answer is what you are doing doesn't make sense. It further sounds like that you wanted your code above to own or assume the identity of the argument view (your reference to not being able to assign to self). If this assumption is correct, your code would make even less sense - you would just need to assign to a variable.
Just create the view class, with the code that you have posted, you do not need to implement a constructor, since you have provided a default value for your selected variable. Your will with then be populated from IB via the coder based constructor.
If you are trying to clone or copy a given view, then refer to "Can UIView be copied"