قالب وردپرس درنا توس
Home / IOS Development / View-state powered applications

View-state powered applications



Cocoa applications are traditionally "presentation-driven". Thus, I mean that impression changes begin by first selecting the presentation change – as a display control segment – and later we set up the data to provide the display content.

In this article, I will look at why a presentation-driven approach is in conflict with the primary application design rule discussed in the previous article. I look at some constraints caused by this conflict and look at what happens when we reverse the steps of first setting data – a coding of impression intensity – update later presentation to match this data.

To show some possible benefits of a computer-controlled approach, the code in this article will show you how to implement complete user interface logging, full state recovery and user interface "time travel" in an app that stays as close as possible to Xcode's "Master-Detail App "project template.

This article will be a more thorough look at the same topic and trial project (significantly updated) as I presented at the test! Swift NYC 201

7. The video for the current talk is available if you prefer to start there.

Content

Ideal Model View Controls

Ideally, in a Model View Controller program, user actions take place along the following data stream path:

Ideal Data Interface in a Model View Controller Program

A user action is received by a viewer that targets the controller. The controller performs an action on the model. Neither the targeted reviewer nor the interactive view make any changes at this time – they only propagate an event.

The model makes any changes that fit the action and sends an alert. Any reviewer interested in the notice may observe it. Observe Controllers Updates Views according to the changes described in the notification.

These are ideal model viewing controls. User actions always propagate to the model. No condition is changed before the action takes place on the model. Impressions are updated solely as a result of alerts from the model. As I discussed in the previous article, these requirements retain the perception of the model as an abstraction. Time

To demonstrate a typical Model View-Controller app, I wrote the following application.

You can see this version of the app on the "undoredo" branch of the clock github depot.

The app is a basic world clock. The main screen displays a list of time zones. You can press the "+" button and add new time zones from the "Selection" screen. You can set "Edit" mode on the main screen and delete existing time zones. You can tap a time zone on the main screen to display a detail view. You can rename the time zone in the detail view.

It's not the functionality of the app that's important, but the fact that the app is as close as I can do is directly based on the Xcode project template for the Master-Detail App and follows the same basic formula that comes with Apple's sample code.

Let's look at the code of usage for this app. Code of MasterViewController to delete a Time Zone from the list is:

  // Controller receives delegated action from View and invokes Model Action 
  Override    Func    ] Tableview   (  _    Tableview :    UITableView     commits    editingStyle : 
     UITableViewCellEditingStyle     forRowAt       ] indexPath .:.    IndexPath )    {
     document     shared     removeTimezone   (  sortedTimezones [19659022] ] ] ]   row ].   uuid ) 
} 

  // Document informs Change and Controller updates See corresponding 
  func   ] handleDocumentNotification   (  time zones :    Document     data type  [1 9659023] action .    D document .   Action ?)    {
     Breaker    action    {
     case  .   removed   [ ]    UUID ):? .. ​​   index    =    sortedTimezones     index [19659035]   $ 0     UUID    ==    UUID  } [1965909419659095] []  updateSortedTimezones   (  time zones :    time zones  time zones  ]) !.   Tableview   [19659041] deleteRows   (  at :    [  IndexPath   (  p :    index       ] 

]

] with : . Automatic ) } }

These two features show all the steps from our "ideal model view-controls" chart. The first feature shows the action from the display and the call on the model. The other feature shows the observation of the model alert and the application of changes in the display.

Do you know what your app is doing?

In the Clock app, each time the model changes, a snapshot of the model is saved. The app shows a slider at the bottom of the screen, and by using the slider, you can rewind the model through each of the snapshots.

The effect is simple : Slider and tools undelete and repeat using snapshot s of the model. [19659002] However, try the following steps:

  1. Go to the "Selection" screen and add a new time zone
  2. from the main screen, select "Edit" mode in the navigation bar
  3. delete the newly added time zone and leave "Edit" mode by selecting "Done"
  4. Use the slider at the bottom of the screen to slow down these changes.

The allowed time zone will appear again since the deletion has been deleted and then disappears once the creation has been deleted. However, the "Edit" mode will not appear again, and the screen will not appear again.

A video about adding and deleting "Casablanca" and then using the slider to undo and repeat this change

We say that the view presents the model and the model is the true representation of the app, but this simple app shows that it is a lie. The model is not a complete representation of the state of application . The display is not a simple display of content from the model. The "Selection" screen is not part of the model, but it affects the screen. "Edit" mode is not part of the model, but it affects the screen. Many user actions in the typical Model View-Controller app do not follow the ideal data flow path and change the state without reaching the model.

Separate Pipelines

 Broken MVC

Presentation-Driven Changes Include Display and Controls That Work Without Model

In typical MVC cocoa, as this chart shows, segues and other "presentation" change - as the appearance or disappearance of SelectionViewController in the Clock app - handled completely within the control layer, leaving the model out of loop.

It's not just a problem with "presentation" changes. Many common View and Controller classes in cocoa are built around, they believe that they can be built, updated and mutated without a model being involved at all. It is common that any of the following data exists only in Views or Controllers, and not in the model:

  • Navigation pile in a navigation control.
  • Rolling position for a scroll view. 19659131] Line selection in a table view.
  • Lose selection in a flip view.
  • Condition of switches and sliders.
  • Unpublished text field.

These data outside the model are called "View State". This is a collection of the most user-friendly state of our application, and the default method of the Model View Controller is to ignore it.

Thinking about what should be a model

Yes, I know that the traditional Model-View-Controller intentionally excludes View-state from the model. Traditional formulations of the model describe persistent data to the app, and all non-persistent is excluded. The intention deliberately kept unlimited data, so validation and other business logic could be delayed into a "commit" step. RAM for multiple copies, CPU for validation, HD space for continuous storage - These things were slow and very limited resources.

But in modern applications, we have the CPU stream to validate everything continuously, we have additional RAM to hold multiple copies of state and viewing is ongoing between app launches. The assumption that non-document status is not worthy of the same benefits as Document Status lacks justification and counteracts programmers from properly abstracting non-document status.

If we require clean model extensions for the document, I do not think there is a good reason to release View State - or any other state - from the same requirements. I think that the fact that we can not rewind the entire user interface as easily as the document is an indication of lack of coordination and management of the state has left us beyond control of our own application.

I think we should extend the definition of model to:

All mutable state directly represented in View - whether it is stored in Document, View Mode, User Settings, File System, System services, networks, or anywhere else - is a model and should be pure abstract from the view. Lack of isolation of this mutable state in a model abstraction represents an error in application design.

According to this rule, dogmatically, I should have united use of Hours and Date under a

Model for View-state

We Need a Model Exception for View -state.

If you & # 39; I have never treated View-state as a model before, this can be a weird statement to do. To understand what is required, let's start by looking at the document model that we already have.

The existing Document Model

The document model in the app is called Document and provides the following mutating methods:

  func    addTimezone   (  _    :    String ) 
  func    updateTimezone   (  _    UUID :    UUID     NEWNAME :    String ) 
  FUNC    removeTimezone   ([19659023] _    uuid :    UUID ) 

Understand the actions we can perform on the model, we can understand the model. These actions lead to the internal representation of the document

:

private var time zones : [ UUID : Time Zone [19659022] Time Zone [19659022] ] = [:]

struct time zone : Codable {
la UUID : UUID [19659205] ]

] String
var name : String
]

The primitive storage for The document is only a collection of identifier and name pair, typed by uuid in a dictionary.

Our new ViewState model

Let's look at the View-State in a similar way. I have already discussed the most important actions a user can perform:

  • Touch a line in the main table view to change the detail
  • enable or disable "Edit" mode on the main table view
  • show or hide the new time zone selection view

It is also rolled in both the main table view and the new time zone selection view and a search field in the new time zone selection view for filtering the list.

It is a complete list of user actions that do not. This will not immediately lead to a change in the document . It provides the following list of mutating methods for our new ViewState :

  func    scrollMasterView   (  offsetY :    Double ) 
  FUNC    scrollSelectionView   [ ] :    Double )    FUNC    changeDetailSelection   (  UUID :    UUID [19659022] ]    FUNC    changeEditModeOnMaster   (  _    isEditing :    Boolean ) 
  FUNC    changeSelectionViewVisibility   ([19659023] _    visible :    Boolean ) 
  FUNC    selectionViewSearchString   (  _    value :    String ) 

] We could represent the values ​​as these methods change as 6 properties on a single structure, but I think it is better to organize these properties ne as a tree of structures that reflect the actual controls ] var

] splitView : SplitViewState = SplitViewState ()


: MasterViewState
var detailView : DetailViewState
var selectionView : ??

struct MasterViewState : Codable {
var masterScrollOffsetY : Double = 0
Var isEditing : Boolean = false
}

struct SelectionViewState : Codable [19659035] {
was [19659079] selectionScrollOffsetY [19659022]: Double = 0
var SEARCH : String = "
]

struct [19659201] : {
la uuid : UUID
}

See about 30 lines of the declaration code in this section. These statements are an abstract representation of the entire 600-line program.

From presentation driven to View-state drive

We look at how this changes our code, but first we see how a "presentation" changed

  // [Cont:    UITableView     didSelectRowAt    indexPath :    indexPath )    {
     performSegue   (  withIdentifier [19659022]  ]  ]   ]   ]      sends :    tableView ) 
] 

  // Controller sends data to segue destination 
  FUNC    prepare      to    natural transition :    UIStoryboardSegue     sender : [19659167] All ?)    {
     if    natural transition .   identifier    ==    "showDetail"    {
         if    la    indexPath    =    Tableview .   indexPathForSelectedRow    {
             la    sonen    =    sortedTimezones   [  indexPath [19659022].  ]   ] 
             la    regulator    =    (  natural transition .   destination    som ! 
                UINavigationController   19659024]).   topViewController    as !    DetailViewController 
             regulators .   UUID    =    sons .   UUID 
             regulators .   navigationItem .   leftBarButtonItem    = 
                splitViewController .   displayModeButtonItem 
             regulators .   navigationItem [19659022].   Left ItemsSupplementBackButton    =    true 
       } 
   } 
} 

As clumsy as this code looks, it's almost unchanged from the "Master-Detail App" template. This is a presentation driven transition. It begins by asking a reviewer to change the display tree in the table view (_: didSelectRowAt :) function and then under the display tree change in prepare (for: sender :) ] There is so much to dislike about this code, but from a model exploitation perspective, the biggest problem is that the code is never clear codes for its purpose . The line performSegue implies that the purpose is merely to "displayDetail", but it is not true: the purpose is to display the selected timezone.uuid in the detail view. The awful ugly work performed in prepares (for: sender :) function, mostly tries to reconstruct the key function for the purpose of the crash scene wreck left on the controller and tableView ].

Now let's look at this code in a View-state-driven approach.

  // MasterViewController receives delegated action from Show and summon performSegue 
  override    func    tableView     _    Tableview :    UITableView        ] didSelectRowAt    indexPath :    IndexPath )    {
     Display Status [19659022]   shared     changeDetailSelection   (  UUID  ] ] ] 
} 

  // SplitViewController receives delegated action from Vis and / or . . IndexPath     p ]   uuid           Action :    Action :    SplitViewState     Action :    19659024].)    {
     Breaker ~~ POS = HEADCOMP    Action    {[1965906] 4] case  .   changedDetail      where    state .   detailView 

= nil : masterViewController performSegue ( withIdentifier :?. "details" ] : ] // Other Actions omitted } }

We apply an action on ViewState which changes ] first – immediately declare an intention to change the details of the selected UUID – as SplitViewController (not MasterViewController ) observes that data change and u update the detailed presentation in response. We never need to put uuid directly on DetailViewController DetailViewController subscribes to ViewState and learn how its uuid is thence.

Compare this code sample to the "Ideal Model View-Controller" model change back in the Clock section. The two changes follow an almost identical series of steps.

What do we get?

You can find ViewState build on the "master" branch of the clock github depot.

Time-travel

Unlike the "undoredo" branch in this project, the slider control on the "master" branch is full time travel .

A video about adding and deleting the "Casablanca" slider to locate the entire user interface back and forth through the action.

Ironically, for an app that shows the world's time, time travel will not affect the time shown on any of the bells since (enter from Hours and Date is deliberately excluded from ] HistoryViewController ).

What time travel means that the slider at the bottom of the screen will rewind ViewState at the same time as Document resulting in an An all-app user interface that can be wound forward and backward with the slider. Editing mode will turn on and off, the detail view will come in and out, the display will appear and disappear, scroll position will adjust, unpublished text fields will turn forward and backward as required text field.

If you & # 39; I have never seen a time-consuming user interface before, I strongly recommend running the project and playing with the slider. It should not be surprising to see a program immediately and completely respond to commands, but it is . We are used to being continuously out of use of our own applications.

The slider has a UISwitch on the left. When you enable the slider, it is split into separate sliders for "Document" and "View Mode", so you can manipulate the two independently. This will occasionally result in quirks where "View-state" has to be changed to unite it with "Document," but it is proof that the two are really separate models.

ViewState acts as a "coordinator"

With a View-state model, separate controllers can implicitly coordinate by tracking the same data in the View-state model. There is no longer a need for MasterViewController to cross through the display and control hierarchy to specify values ​​on DetailViewController DetailViewController can simply ask ViewState ] for its values.

In a presentation-driven app, it is an instinct to handle the full path of all actions on the controller where the action is received. This dumps a lot of work on MasterViewController although it is not parent for many of the actions it coordinates.

With a View-state model, the presentation of SelectionViewController ] and DetailViewController can be administered by the actual parent of these actions ( SplitViewController ) instead of to try to handle all this work from the table view in MasterViewController .

Some design patterns use a "coordinator" to actively convey communication between controllers. ViewState fulfills a similar role through passively observed model data, rather than actively propagated control actions.

Persistence is trivial

In a typical Cocoa app, we trust Storyboards to help persistent viewing controllers. Even with the help of Storyboards, it is often UIStateRestoring or UIDataSourceModelAssociation which is working to fully restore our state.

With ViewState commits everyone and verifies work is already a part of ViewState observing pipeline so everything is automatic. No UIStateRestoring or UIDataSourceModelAssociation required. In fact, you do not even need Storyboards.

Provided that you already handle ViewState the notices correctly, this is all code required for state restoration:

  FUNC    application   (  _    :   :             willEncodeRestorableStateWith 
     encode :    NSCoder )  {19659029] encode .   Code   (]      Display Status .   Shared .   Serialized   (),    Forkey . [             :       UIA application     didDecodeRestorableStateWith         
    FUNC    19659029] :    NSCoder )    {
    ]    la    data    = [19659024] coder . [19659041] decodeObject   (  Forkey .      viewStateKey )?    as      data    {
        Display Status     .     

] } Scenario Recovery Mode 19659160] Since the Restore of the UI is no longer associated with UIStateRestoration or Storyboards, you can always jump to a specific user interface configuration - speed development and troubleshooting.

  la    jsonString    =    "" "
     {"  detailView ": {"  UUID ": "  8642   FA12  -   D3F7  -   48   B6  -   B308  -   3   B415A7145D0  "},
     "  Master ": {"  masterScrollOffsetY ": 0, "  isEditing ": false}}
     with    jsonData    =    jsonString     data   (  with .:      utf8 )  
 Display Status     Shared     ReloadAndNotify   (  jsonData :! ..    jsonData ) [19659125] If your document also supports arbitrary relay (like  The document  does the app at the time), you can combine the two to jump to a troubleshooting or test scenario at any time. 

Troubleshooting Information

The entire view can be logged to the console at each step, enabling better troubleshooting - you can at any time know the exact state (and previous transitions) so you can quickly find the cause of trouble.

The Clocks app logs both document and display status when it changes:

  Changed document to:
["CC0D7C93-3FBC-4D89-9127-ABBF798420F6",{"identifier":"Australia/Melbourne",
"name":"Melbourne","uuid":"CC0D7C93-3FBC-4D89-9127-ABBF798420F6"},
"54FA679F-6036-431D-957F-6E8CC1B8DA50",{"identifier":"Africa/Addis_Ababa",
"name":"Addis Ababa","uuid":"54FA679F-6036-431D-957F-6E8CC1B8DA50"}]
Changed display state to:
{"DetailView": {"UUID": "CC0D7C93-3FBC-4D89-9127-ABBF798420F6"}, "Master":
"MasterScrollOffsetY": 0, "isEditing": false}}

This logged state can be used directly in state restoration - as in the previous example - to restore the scenario at any time.

What is the cost?

Code Size

Tracking of viewing mode is usually more code. This is no different than any kind of model - there are more statements and more work to set and observe changes that go into and out of the model extraction.

Measure the file size ".swift" (except the Utilities folder) using the command line tool cloc:

  • The "utoredo" version of the project is 437 lines
  • The "timetravel" version is 610 lines

85 lines of This difference is the "ViewState.swift" file. The remaining 88 line change is more complicated; It's more like a 150 line change, sometimes an increase, sometimes a decline.

Without having to go through ViewState the "undored" branch has some buttons linked directly to Storyboard segments. The "Undoredo" branch also does not track (the "timetravel" building may exclude this, but it's a part of making time trips look magical.)

Tracking mode does not always increase the size of the code. For some tasks it is simplified. DetailViewController actually has the benefits of not controlling even when The time zone it observes is deleted from Document . All "Prepare for Victory" methods are removed from MasterViewController . All UIStateRestoring methods have been removed.

Many observations

In an app without viewing mode, you may only need to observe an object - document model - in each checker. If you add View-state to this, your observation needs will be doubled.

My specific implementation of the observation of The document and ViewState doubles this again ]. Take a look at tradeViewStateNotification for SelectionViewController :

  func    tradeViewStateNotification   (  state :    SelectionViewState     action :.?. 
     SelectionViewState     action )    {
     break ~~ POS = HEADCOMP    action    {
     case    ] [19659041] changedSearchString :?.    updateForSearchString   (.   state     sEARCH ~~ POS = TRUNC ) 
     case      rolled :  Tableview  ].   contentoffice .  y   =   CGFloat  ( state .  selectionScrollOffsetY ) [19659071] saken    ingen :.?.. 
       Tableview   contentOffset   y   =   CGFloat  ( tilstand .  s electionScrollOffsetY)
      searchBar?.text = state.searchText
      updateForSearchString(state.searchText)
   }
}

This function has to handle two possible ViewState actions – “search string change” and “search table scrolled” – but it also has to handle a third .none case which is used when the ViewState is reloaded and everything needs to be reprocessed.

It would be ni ce to avoid this redundancy but I’m not sure how to do this cleanly since sometimes, responding to an already applied action and restoring state requires slightly different logic (see the need to set the searchBar?.text during restore which isn’t needed on .changedSearchString because the user has alr eady set this value).

Uncooperative UIKit/AppKit classes

The hardest part when writing an application for the first time in a View-state driven approach is learning how to handle Views and Controllers that fail to precisely notify when changes occur and refuse to accurately adhere to state they are given.

To be clear: none of these are show stopping problems, nor even particularly difficult to manage, but they all require guesswork and multiple steps since Cocoa classes were never written to clearly notify changes in state or follow programmatic (rather than user) instructions.

Conflicting actions

Multiple “present” animations are not permitted to occur at the same time – so if you’re trying to restore two pieces of state simultaneously, these might need to be queued – otherwise the app will raise an exception. This causes problems in the Clocks app when the modal “Selection” screen and the “Detail” view need to be restored at the same time. The SplitViewController class contains some careful sequencing interaction between selectionViewController and reloadDetailView to avoid problems.

A less fatal conflict occurs when trying to set a scroll position during UITableView reloads. The UITableView reloads asynchronously over an indeterminate amount of time. There is no way to queue the scroll position change to occur when this operation is complete. When a tableView.reloadData() occurs, MasterViewController does not bother to set the scroll position at all since I was not able to find a reliable way to both reload data and scroll.

Poorly notified changes

UINavigationController may change its navigation stack without explaining why when the user taps the back button. The navigationController(_:didShow:animated:) method is forced to guess what may have happened.

Detecting scroll state presents a slightly different problem: to detect when scrolling has ended, you need to implement two separate delegate methods – scrollViewDidEndDecelerating and scrollViewDidEndDragging – and carefully check the decelerate parameter on the latter to determine if it is actually the end of scrolling.

User-actions with no clear programmatic equivalent

In portrait on an iPhone, the detail view of the split view will also collapse onto the master view’s navigation stack. The user can tap the back button to remove this collapsed view but there’s no clean action to ask the split view to do it. It’s necessary to check the UINavigationController and guess whether the top view is a collapsed detail view and use popViewController to evict it. Messy work.

Conclusion

You can explore the two versions of the Clocks app on github:

(Minor note: when debugging one branch after using the other, you may see non-fatal state restoration errors as the two apps contain incompatible state restoration formats.)

The purpose of this article was to present an approach for tracking View-state in an application that is otherwise as close as possible to a typical Cocoa Model-View-Controller application.

I would argue that tracking View-state in a Model is more true to the spirit of Model-View-Controller than the traditional “ignore View-state” approach. When you track View-state in a Model, then the sum of Models in your app is truly a representation of your entire app and your View is the simple display of the Model that it should be, rather than being a complex combination of Model plus other untracked state.

As I write this article, I have now written a handful of trivial to small apps using this pattern. It manages navigation and state restoration really well while offering good conceptual clarity and excellent debugging capabilities. I think I’ll continue to write all small MVC apps this way. Observing both View-state and the Model on each Controller does get a little tiring and some Cocoa classes can feel truly obstinate when you’re trying to get them to behave precisely and punctually but the balance still feels strongly in favor of this approach.

There are numerous frameworks that aim to simplify issues with observing multiple Models or driving Cocoa classes through data by using reactive programming or reducers. However, these bigger frameworks increase the distance between the code you write and the effects of that code. The advantage to this View-state approach is that there is no real framework “in the way”.

The time-travel capability in the Clocks app is fun but you’d never ship time-travel in a real application. Even when debugging, simple View-state logging and the ability to restore your UI from JSON at any time are far more useful tools. I actually keep View-state logging on all the time because it is really helpful when something goes wrong – you can immediately understand if the problem was due to mis-setting View-state or mis-interpreting/observing.

However, time-travel is conceptually important for programmers who have forgotten that our programs are supposed to precisely and punctually obey our intent. Speaking for myself, I know that I had grown accustomed to boringly repetitive human-driven actions to arrange the user-interface while developing or testing. Launching the debugger directly into the precise state that I’m developing and testing has huge benefits (yes, UIStateRestoration is supposed to do this but it’s far more opaque and difficult to control).

Looking forward

One of the biggest drawbacks of this approach is the need for extensive observing code on the view controller that grows more complex with view properties that are interdependent on both the View-state and the Document.

In the next article, I’ll start by simplifying these observations with reactive programming and then continue to improve the syntax by building views in code with all behaviors fully specified.

Self promotion? On my own blog?!

This pattern, along with other experimental and conventional patterns and architectural techniques, are examined in depth in a book I’m writing with Chris Eidhof and Florian Kugler from objc.io titled App Architecture.

App Architecture

You can order now in Early Access to get the first chapter immediately and subsequent chapters as they’re released.




Source link