INTRODUCING A LIBRARY FOR DECLARATIVE LIST USER INTERFACES ON MOBILE DEVICES - CASPER HEDBRANDH STRÖMBERG - DIVA
←
→
Page content transcription
If your browser does not render page correctly, please read the page content below
UPTEC IT 20015 Examensarbete 30 hp Juni 2020 Introducing a library for declarative list user interfaces on mobile devices Casper Hedbrandh Strömberg Institutionen för informationsteknologi Department of Information Technology
Abstract Introducing a library for declarative list user interfaces on mobile devices Casper Hedbrandh Strömberg Teknisk- naturvetenskaplig fakultet UTH-enheten Developing user interfaces that consist of lists on native mobile platforms is complex. This project aims at reducing the complexity for application developers when Besöksadress: developing dynamic interactive lists on the iOS-platform by creating an abstraction Ångströmlaboratoriet Lägerhyddsvägen 1 that lets the application developer write code on a higher abstraction level. The result Hus 4, Plan 0 is a library that creates an abstraction that developers can use to build list user interfaces in a declarative way. Postadress: Box 536 751 21 Uppsala Telefon: 018 – 471 30 03 Telefax: 018 – 471 30 00 Hemsida: http://www.teknat.uu.se/student Handledare: Jesper Sandström, Spotify Technology S.A. Ämnesgranskare: Anca-Juliana Stoica Examinator: Lars-Åke Nordén UPTEC IT 20015 Tryckt av: Reprocentralen ITC
Sammanfattning Långa listor med dynamiskt innehåll är idag en viktig del av användargränssnittet i många applikationer. Vi använder dem varje dag i olika sammanhang, vare sig det är för att visa en lista med låtar i favoritspellistan på musikstreamingappen, eller för att hitta det rätta tåget i kollektivtrafiksapplikationen. Sådana listor har historiskt sett varit svåra att utveckla, vilket har resulterat i felaktiga program eller en långsam utvecklingstakt. Den här rapporten presenterar en lösning för smidigare utveckling av listor. Detta görs genom ett bibliotek, vars syfte är att göra det enklare för applikationsutvecklare att programmera dessa listor. Lösningen innebär ingen skillnad för användaren, men minskar tiden för utveckling, vilket i sin tur kan ge mer tid till utveckling av nya funktioner.
Acknowledgements I thank Jesper Sandström and the rest of the App Architecture team at Spotify for all the guidance and great feedback I received from you. I would also like to thank Anca-Juliana Stoica for all the feedback and input that made this thesis possible. I would like to thank my fellow students, colleagues and best friends Erik Eng- berg, Lowe Eklund, Fabian Haglund and Jonathan Wahlgren for all the moments we shared and for the ones that we will continue to share in the future. Finally, I would like to thank all of my friends and my family for being there for me both during this thesis work, but more importantly in times of need.
Contents 1 Introduction 1 1.1 Lists in User Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Aim and Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.3 Requirements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.4 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.5 Research Question . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2 Background 5 2.1 Diffing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.2 Reconciliation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.3 Viewport . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.4 Virtualization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.5 Recycling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.6 UIKit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.7 UIKit lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.7.1 Responding to data changes . . . . . . . . . . . . . . . . . . 12 2.7.2 Source of truth . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.7.3 Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.7.4 Index Paths . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.7.5 Recycling . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.8 Separation of Concerns . . . . . . . . . . . . . . . . . . . . . . . . . 16 3 Result 16 3.1 Application Programming Interface . . . . . . . . . . . . . . . . . . 17 3.1.1 TableRender . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.1.2 TableContext . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.1.3 Row . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.1.4 Custom Rows . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.1.5 Headers and Footers . . . . . . . . . . . . . . . . . . . . . . 19 3.1.6 Automatic Section Creation . . . . . . . . . . . . . . . . . . 20 3.2 Implementing Table Renderer . . . . . . . . . . . . . . . . . . . . . 21 3.2.1 Context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.2.2 Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 3.2.3 Renderer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 3.2.4 Reconciler . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4 Discussion and Evaluation 24 4.1 Composability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 4.2 Declarative . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 4.3 Type Safety . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.4 Coupling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 4.5 Cohesion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 4.5.1 Example: UIKit Implementation . . . . . . . . . . . . . . . 29 4.5.2 Example: TableRender implementation . . . . . . . . . . . . 30 4.6 Mixed Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 4.6.1 Example: Mixed lists . . . . . . . . . . . . . . . . . . . . . . 32 4.6.2 Example: Mixed list with UIKit . . . . . . . . . . . . . . . . 32 4.6.3 Example: Mixed list with TableRender . . . . . . . . . . . . 34 4.7 Flexibility and Extension Points . . . . . . . . . . . . . . . . . . . . 35 4.8 Data updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 5 Related Work 36 5.1 SwiftUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 5.1.1 Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 5.2 React . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
5.3 Cycler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 5.4 Di↵able Data Source . . . . . . . . . . . . . . . . . . . . . . . . . . 39 5.5 Litho . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 5.6 ComponentKit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 6 Conclusion 40 6.1 Future Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 6.1.1 Collection View . . . . . . . . . . . . . . . . . . . . . . . . . 41 6.1.2 Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 6.1.3 Declarative UI . . . . . . . . . . . . . . . . . . . . . . . . . . 42 References 42
Glossary Application Programming Interface An interface, generally specified as a set of operations, that allows access to an application program’s functionality. This means that this functionality can be called on directly by other pro- grams and not just accessed through the user interface. 4, 5, 40 cell A unit that is responsible for the user interface of a single piece of data in a list (e.g. rows, headers, and footers). 1 cell-type A class of cells that has similar behaviour and therefore can be reused together by re-configuring the containing elements. For example, a list can have a song cell-type that displays songs and an album cell-type that displays albums. 1 iOS A mobile operating system developed by Apple Inc used exclusively on its mobile devices. . 3–5, 10, 11, 16, 36, 39–42 library A set of reusable concrete and abstract classes that implement features common to many applications in a domain (e.g. user interfaces). 1, 3, 4, 8, 16, 19–21, 23, 27–29, 35, 37, 38, 40–42 Swift A modern general-purpose programming language used as the default lan- guage on the iOS-platform. 3
List of source codes 1 The result of a naive diffing algorithm with false positives. . . . . . 7 2 The result of a diffing algorithm with maximum preciseness. . . . . 7 3 Example of a declarative API that requires the use of reconciliation and diffing (from TableRender library) . . . . . . . . . . . . . . . . 8 4 Example of the usage of index paths when deciding what action to perform based on an event-triggered from a row. . . . . . . . . . . . 15 5 Example of a registration of a table view cell. . . . . . . . . . . . . 15 6 Example of the dequeue and binding process for a table view cell. . 16 7 Example of the initiation of table renderer . . . . . . . . . . . . . . 18 8 Example of the usage of TableContext to construct a list. . . . . . . 18 9 Example of a selectable row. . . . . . . . . . . . . . . . . . . . . . . 19 10 Example of a custom row provided by the application developer . . 20 11 Example of a simple header . . . . . . . . . . . . . . . . . . . . . . 20 12 Example of sections that are created based on headers and footers. In the example 4 sections are created. The first one with a section header, footer and one row. Sections 2 will only contain a header while section 3 will contain a header and a Footer but no rows. The last section will only contain a row . . . . . . . . . . . . . . . . . . 21 13 Example of composition using the Context API . . . . . . . . . . . 23 14 Example of a simple list with one section that has a header and a footer which contains a row. . . . . . . . . . . . . . . . . . . . . . . 25 15 Composition is achieved through functions. . . . . . . . . . . . . . . 26 16 Common implementation of a method that provides cells in the table view data source. Note the unsafe typecasting on line 4 and the risk of invalid access to arrays on line 1. . . . . . . . . . . . . . 28 17 Example data model . . . . . . . . . . . . . . . . . . . . . . . . . . 32
18 Example: An implementation using the pure UITableView imple- mentation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 19 Example: Multiple cell-types with TableRender . . . . . . . . . . . 34 20 Example of a cell that is configured to be editable. . . . . . . . . . . 36 21 Example of a list with two sections built with SwiftUI . . . . . . . . 37 List of Figures 1 A typical scrollable list that demonstrates the usage of di↵erent cell-types and multiple sections with headers . . . . . . . . . . . . . 2 2 (a) Example of a heterogeneous list where di↵erent cell-types are used to display multiple types of data on the same list. (b) Example of a homogeneous list where only one type of data is displayed in the same way trough-out the list. . . . . . . . . . . . . 3 3 Overview architecture of TableRender . . . . . . . . . . . . . . . . . 22 4 Implementation of a list with di↵erent cell-types using pure UIKit. . 29 5 Implementation of the same list from Figure 4 using TableRender. . 30
1 Introduction The development of the user interface is a large part of many applications. Many features of modern mobile applications depend on long lists with dynamic rows of di↵erent types. Displaying long lists of dynamic data in a highly efficient manner presents a challenge for many application developers. In the traditional imperative method of programming, the application programmer is responsible for adding, deleting, and moving items as the underlying data are changing. Additionally, several performance optimizations that increase the complexity are required to make the user interface responsive. By using a library that allows the application developer to write declarative code many of the intricacies can be abstracted away. The project will try to utilize methods such as reconciliation and diffing to be able to construct this abstraction. Spotify Technology S.A. is an audio streaming service that relies on having great scrolling user interfaces. This project is done in collaboration with Spotify to try to fix the issues with the current solutions. 1.1 Lists in User Interfaces A list is in the context of user interface development a user interface component that displays a collection of data. The collection of data can be homogeneous and only describing one type of repeating data e.g. a list with all of the users’ favourite songs or it can be heterogeneous and displaying di↵erent types of content on the same list. A single piece of content is called a cell. Di↵erent types of content often have di↵erent layout and behaviour, this is when the concept of cell-types is used. A cell-type is a class of cells that look and behave in similar ways often mapped to one specific data type. Many lists can grow dynamically and therefore grow to be larger than what 1
Figure 1: A typical scrollable list that demonstrates the usage of di↵erent cell-types and multiple sections with headers a device can display at the same time. To show larger lists than the display can fit the list can be made scrollable so that a select part of the list can be shown to the user and the user can choose to see more if needed. A common pattern in applications is to show multiple lists in the same scrolling surface with di↵erent content, the multiple lists are often considered to be one list with multiple sections. List with deeper hierarchical nesting exists but will not be explored in this thesis. 2
(a) Heterogeneous List (b) Homogeneous List Figure 2: (a) Example of a heterogeneous list where di↵erent cell-types are used to display multiple types of data on the same list. (b) Example of a homogeneous list where only one type of data is displayed in the same way trough-out the list. 1.2 Aim and Method The goal of the project is to implement a prototype of a library that should be able to take a list of data and specifications of each list item and render it to screen as well as keep the interface updated as the data changes. The prototype will focus on mobile applications in general and iOS devices in particular. Mobile devices are focused due to two factors that make the prototype more useful than on other platforms; mobile devices have small screens and scrollable content is therefore used extensively, many mobile devices are low powered and applications that run on the need to be efficient to provide a good user experience. The project will be using Swift as the implementation programming language [15]. The project will 3
follow a building methodology as defined by [27]Amaral et al. The project requirements are to provide an abstraction that is easier to use than the platform default while still be powerful enough to allow for complex user interfaces to be built on top of it. The library that is presented should be flexible enough that it can be used in existing projects that use di↵erent idioms and frameworks. The library should work on large applications where many teams are involved in developing the same feature. 1.3 Requirements The requirements of the library can be summarized as follows: • The Application Programming Interface should be declarative. • It should be possible to customize the rows and their behaviour as much as the underlying system allows. • It should make it easier to produce lists with di↵erent cell-types. • It should make it easier for di↵erent teams to work on the same list without interference. • It should enable rows, headers, and footers to be reusable, not coupled to a specific list. • It should enable updates in data to be propagated into the user interface automatically. • The solution should not only work on the most recent operating system (iOS 13) but also the work on devices that has older versions of the operating systems installed (iOS 11+). 4
1.4 Scope Although the techniques and findings of this project are not specific to a particular platform or type of device, the scope of the project has been limited to mobile devices running the iOS-platform. The thesis is done in collaboration with a large technology company and the solution is therefore geared towards solving problems that occur in large organizations with large code-bases rather than problems that occur in smaller systems and organizations. 1.5 Research Question The project aim is to provide a more productive declarative Application Program- ming Interface for lists as an alternative to the native list Application Programming Interfaces. To investigate and develop the abstraction needed to achieve this goal the following research question is formulated: Can an abstraction be constructed to provide a declarative interface to native list user interfaces on the iOS-platform? 2 Background The iOS-provided Application Programming Interface for build user interface lists are in general not declarative but instead, are built to facilitate imperative object- oriented approaches to user interface development [25]. While imperative and object-oriented approaches have proven to be valuable when developing user inter- faces they provide low-level abstractions on which great responsibility on how the user interface should be constructed is handed o↵ to the application developer. As a contrast, declarative approaches to user interface design focuses on what should be rendered to the screen and not how. The declarative model lets the application 5
code not have to consider every state transition that needs to occur to update the interface to the desired state [7]. Instead, the application only has to consider what the current state should look like and the underlying system takes care of what has to change for the interface to match that expectation. For the platform to take on this responsibility, techniques for calculating changes and syncing those changes to the underlying systems are required. The most important techniques used in this project are summarised below. 2.1 Diffing Diffing is the process of working out the di↵erence between two data sets. There exists a myriad of diffing algorithms aimed at solving the problem for di↵erent data- structures, with di↵erent performance characteristics, and with varying precision. With precision, the number of false positives is usually the metric considered, and algorithms that can produce any false negatives are considered to not be a valid solution. Some diffing algorithms, however, relax the requirements of finding all changes to speed up the algorithm thus producing false negatives. To see the relationship between preciseness and compute time we can consider the most basic diffing algorithm we can think of: remove all the old data followed by adding all the new data, the algorithm will result in the correct result but will for most cases result in a lot of false positives and is thus a low precision algorithm (see Listing 1). With this example, we can also notice that consideration of how the data usually change needs to be done: if it is common for all of the data to change at once the algorithm where all the old data are removed and all the new data are inserted might be a viable diffing algorithm. The most precise solution is considered to be a solution with no false positives and no false negatives (see Listing 2). This can also be framed as the solution with the fewest possible changes, such an algorithm can often be constructed as a graph or path solving algorithm 6
which is the case for one of the most widely used diffing algorithms: The Mayers Diffing algorithm [24]. More deep and complex data structures are in general slower to di↵ than shal- lower structures, however, it is sometimes possible to use some heuristics to relax the exact diffing requirements in favour of execution time [4]. In the context of user interface programming with declarative methods, diffing is used to figure out the di↵erence between the current state of the user interface and the desired state of the user interface. Di↵erences in the current state and the desired state arise from changes in the data that the view is trying to represent. Diffing is often used as a part of a Reconciliation algorithm. diff( ["A","B","C"], ["A","B"] ) = remove ["A","B","C"] insert ["A","B"] Listing 1: The result of a naive diffing algorithm with false positives. diff( ["A","B","C"], ["A","B"] ) = remove ["C"] Listing 2: The result of a diffing algorithm with maximum preciseness. 2.2 Reconciliation Reconciliation is, in general, the process of reacting to changes in requirements and updating a system so that it matches the desired state. In declarative user interface systems, reconciliation is used to synchronize the application state with the view. This enables declarative user interface code to not have to specify what has changed but instead only have to consider how the user interface should look and behave 7
with the current state, the reconciliation will take care of the necessary mutations to the view. In this thesis, only one-dimensional data are considered which reduces the complexity of the diffing and reconciliation phases. When building libraries that should render trees with views more complex methods may be needed[22]. The tableRenderer.render { $0
of the window that the user can see. The window is in this context the entire view with its coordinate system sometimes referred to as the world coordinate system, in the case of lists this is the entire list. The viewport can change based on user actions like scrolling or zooming. This mapping between the window to the viewport is called the window to viewport transformation. 2.4 Virtualization Virtualization or windowing is a method that is used to reduce the memory foot- print and reduce the time taken to render the list for the first time. It is especially e↵ective on lists that extend far beyond the viewport. The technique works by taking advantage of the fact that in cases where lists are long a large portion of the items in the data set are not visible on screen at a given single point in time. The platform therefore only needs to consider items that should be on screen. When the viewport changes i.e the user scrolls and new rows should be rendered, new views are allocated just before they are needed. Instead of allocating and rendering views for all the items in the data on the initial render, the application only has to create views for the data that is visible in the current viewport and can then lazily render rows as they are needed. To show the proper layout of the list the platform requires that we know the size for all items a priori. The sizes are required to show an adaptable scrollbar and scroll to a specific index without requiring the system to render all cells before the index[11]. On some platforms, this requires the developer to specify a function that takes in data and returns the size of the cell. This measurement step can lead to duplication of coupled layout code which can be error-prone. On other platforms, this step can be done internally but can, in turn, result in a negative performance impact. Other drawbacks of virtualization are that if the virtualization engine is using asynchronous rendering and the user is scrolling faster than new views can 9
be created, blank spaces will appear on the screen and the illusion that all the items are always rendered will be lost. 2.5 Recycling While virtualization provides a good optimization for large lists new views have to be constructed for every new item that should appear on the screen. If the user is scrolling fast a large number of views have to be constructed and disposed of when they leave the screen. A large amount of allocation and deallocation will result in higher pressure on the garbage collection. Recycling is a method that is used to combat this problem. Recycling works as the name suggests by reusing views that already are allocated. When a row is considered inactive i.e when it moves out of the viewport, instead of deallocating the view, it is moved to a queue. Later, when a new row should be rendered an inactive row can be dequeued, reconfigured with new data, and re-positioned. By using a queue the number of new views needed to be constructed is reduced which will decrease the time retrieving views. To support multiple di↵erent row types, platforms typically use some sort of identifier [13] that relates a row to a row type. Each row type is then given its own queue with inactive rows that conforms to that particular row type. This is useful if the application requires rows in the same list to have di↵erent requirements, for example, a list that is composed of image and text rows would have a separate queue for the text and image rows. This would allow the image rows to reuse the image view and the text rows to reuse the text views that are contained in the rows. 2.6 UIKit UIKit is a framework provided by Apple that is aimed at constructing user inter- faces for iOS and tvOS apps[8]. UIKit is built with object-oriented approaches 10
in mind. UIKit provides building blocks for UI-development in the form of view- controllers and views. UIKit is currently the main way developers develop user interfaces on iOS, iPad OS, and tvOS. ”The structure of UIKit apps is based on the Model-View-Controller (MVC) design pattern, wherein objects are divided by their purpose. Model objects manage the app’s data and business logic. View objects provide the visual representation of your data. Controller objects act as a bridge between your model and view objects, moving data between them at appropriate times.” 2.7 UIKit lists UIKit provides two interfaces that developers can use to build lists interfaces. UITableView and UICollectionView. The main di↵erence between the two is that UICollectionView is more flexible than UITableView while the table view has a simplified API, for example, UICollectionView can be configured to render more esoteric layouts like grids and carousels. This project will concentrate its e↵ort into providing an abstraction over UITableView however it should be relatively straight forward to extend the solution to also include UICollectionView as a target enabling the developer to take advantage of the flexibility that UICollectionView provides. The prototype will use UITableView instead of UICollectionView due to the UITableView API being less complex and thus less work is needed to provide an abstraction for the API. UITableView provides a set of classes and protocols that can be leveraged by the developer to create scrolling single column user interfaces. UITableView itself is a class that provides the view where the content should be rendered upon. The view is a subclass of UIScrollView which allows the viewer to be scrollable if needed. UITableView also provides two protocols that define properties that 11
can be used to configure the behaviour of the table view: UITableViewDataSource and UITableViewDelegate. UITableView requires an object which conforms to UITableViewDataSource, the object provides the specification of the number of items in the data as well as the specific views for each item in the data. The UITableViewDelegate protocol defines methods that can be overridden to capture events from the table view. 2.7.1 Responding to data changes When the data that the table relies on changes, the table view has to be notified to update the user interface. There are several ways to notify the table view to update the user interface, all with their specific characteristics. The most simple although naive solution is to call the method reloadData() on the table view [12]. The reloadData() method will cause the table view to reload all the data from its data source, all the rows will be re-rendered not considering if the data that the rows were represented had changed or not. While reloading data is the most straight forward way of updating the list it does not enable transition-animations and it can be inefficient on larger lists with small changes. The other way of notifying the table view of changes is to specify exactly how and what changed, this enables transition animations and lets the table view only have to recreate the rows that had changed from the previous data. This tactic can be used if you know or can determine what changed from the previous render, then the developer calls the methods corresponding to inserts, deletes, and moves on the table view. These method calls can be grouped by wrapping the calls in a closure and pass that to performBatchUpdates() [9]. Then the updates are done in batch, hence the animations will occur together. It is often not trivial to know exactly what changed in the data. If the change is due to a direct user interaction e.g. user deleted a row the solution is given however 12
this is not the most common case in modern applications. In many situations the changes can not be trivially extracted; a common feature in modern apps is to have a search field which filters the data in the list below depending on the input value if the user input changes the data should be filtered and replace the old data in the table, in this case, we only know what the result is and not what changed. To solve this problem application developers are forced to implement a diffing solution, comparing the old data with the new data. 2.7.2 Source of truth The architecture that is forced upon the developer when using raw UITableViews to build lists is a view and a controller where the controller is responsible for updating the view as state or backing data is changing [14]. This architecture is keeping one explicit copy of the state in the controller but the architecture requires the controller to also maintain another implicit copy of the current state in the view. When the state changes the controller has to figure out what changed and synchronize the new changes to the view, essentially implementing an own version of a reconciliation algorithm. The lack of a single source of truth makes it harder for the developer to reason about and test the application. 2.7.3 Configuration The main way of configuring and providing data to table views is to use delegation. Delegation is a patterned widely used in object-oriented programming to provide dynamic object composition. The table view can be configured with two di↵erent delegates that can alter the behaviour of the view in di↵erent ways. • UITableViewDataSource: A data source is an object with the purpose of managing the data that the table view is representing. The data source provides information about the data for the table view, this includes the 13
number of sections that the table should render and the number of cells in each section. The data source also provides the views for each row in the table view [17]. • UITableViewDelegate: The delegate is used to configure the behaviour of the table view when responding to user-initiated events like selections, dele- tions, and reordering. The delegate is also responsible for configuring section headers and footers [18]. 2.7.4 Index Paths When developing applications using UITableView the method of identifying spe- cific rows in the table is to use index paths [1]. Index paths are data structures that encode the path to the specific row at a specific time using two integers: one to specify which section the row is in and one integer which encodes wherein the section the row is. Application developers use the index paths to identify and configure individual rows e.g. they are used in the cell-configuration phase when populating rows with content from the backing data or when an event occurs and one of the UITableViewDelegate methods is called and the application decides how to act based on which data the row that triggered the event is representing. The problem with index paths is that they are ephemeral and changing, the identity of the row is related to the position of the row. The index path to a particular item in the data can change if the preceding data changed e.g. a row is inserted or removed. The usage of ephemeral IndexPaths is the source of many bugs where the data changed between the time that the index path was collected and the index path was used. 14
func tableView( _ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let item = data[indexPath.section].rows[indexPath.row] switch item { case .Artist(let name): print("pressed on artist " + name) case .Song(let title): print("pressed on song " + title) } } Listing 4: Example of the usage of index paths when deciding what action to perform based on an event-triggered from a row. 2.7.5 Recycling UITableView provides an interface that developers can use to recycle table view cells. The table view provides a queuing system where the developer can register views that can be recycled. To utilize the recycling behaviour a two-step process is required. The first step is to register all the view types that can be recycled, this is often done in the view controllers viewDidLoad() method. The registration call consists of two parameters: the class that should be instantiated if the queue is empty and an identifier that allows the system to maintain separate queues for di↵erent view types The second step is performed when the table view requires an func viewDidLoad() { self.tableView.register( SongCell.self, forCellReuseIdentifier: "song-cell" ) } Listing 5: Example of a registration of a table view cell. 15
item to be rendered. The application then calls the dequeueReusableCell() method which will try to find a disposed view for the view type specified, if the queue is empty the table view will instantiate a new view based on the class registered in the step before and return the newly instantiated view. The view is then ready to be configured and rendered to screen. let cell = tableView.dequeueReusableCell( withIdentifier: "song-cell", for: indexPath) as! SongCell cell.textLabel?.text = title Listing 6: Example of the dequeue and binding process for a table view cell. 2.8 Separation of Concerns The delegation pattern that UITableViews utilizes allows for easy separation of the event handling and the data handling but makes it hard to separate multiple types of rows or cells. The programming model works reasonably well when only one team is responsible for the entire table. It is common for user interfaces to present many di↵erent types of cells in the same list, the cells can be managed by di↵erent teams in the organizations. It is therefore essential that a row can be changed by the team owning that view, while not a↵ecting the other cells. 3 Result The result that is presented in this report is a library that aims to solve many problems that developers encounter when developing user interface lists on iOS. The library called TableRender provides a declarative API that lets the application describe the desired state of the list. Each time the render method on TableRender 16
is called the library will use diffing and reconciliation to determine what changed and what needs to update. 3.1 Application Programming Interface Table Renderer provides a declarative API that wraps UITableView allowing the developer to write declarative, cohesive, and flexible code. 3.1.1 TableRender To use Table Renderer the application developer creates an instance of the TableRen- der class. The TableRender takes a tableView as arguments to its constructor method, the tableView provided will be managed by the TableRender object (see Listing 7). TableRenderer will take on the responsibility of providing a data source and a delegate as well as keeping the data source in sync with the view. The ap- plication developer should no longer try to configure these properties of the table view directly but instead use the abstraction that TableRenderer provides. The TableRender object has one public method: render((ctx: TableContext) -> TableContext) which is used to render rows, headers and footers to the screen. To keep the ap- plication data and state in sync with the user interface the developer should call the render method whenever any of the data changes. The render method takes a closure as the argument that will provide a table context and expects to return a table context that is populated with the rows, headers, and footers that describes the table. 3.1.2 TableContext The context module aims to collect and compose rows, headers, and footers into a stack-like data structure. The context has two public methods: one that adds an item to the end of the stack and a method that appends the contents of another 17
let tableView = UITableView() let tableRender = TableRender(tableView: tableView) tableRender.render { ctx in ... } Listing 7: Example of the initiation of table renderer context to the end of its stack, this can be used to compose di↵erent logical parts of a list. The context is the main user-facing API which developers use compose lists with TableRenderer (see Listing 8). func Artists() -> TableContext { var ctx = TableContext() ctx
Cell and provide that to the onRender() method. The model is used for diffing and must, therefore, conform to the Hashable protocol which is a built-in protocol in Swift. The conformance to the Hashable protocol means that the library can determine identity as well as equality when running the diffing algorithm. Since the model is used in the diffing algorithm all the data that is used to configure the cell must be included in the model. If the application developer fails to do so can result in stale data rendered to the screen. Row(model: "model") .onRender { (cell, model) in cell.textLabel.text = model } .onSelected { print("pressed")} Listing 9: Example of a selectable row. 3.1.4 Custom Rows For most non-trivial applications the rows need to be customized further than the standard UITableViewCell. TableRender handles this gracefully by allowing the user to pass in a subclass of a UITableViewCell to the onRender method. The custom cell will be automatically registered on the table view with a reuse identifier based on the class identity thus creating a reuse queue that will contain instances of the CustomCell type. An instance of the custom cell will be provided to the render block in the onRender method allowing the application developer to bind the model data to the view. 3.1.5 Headers and Footers Headers and footers are constructed in the same manner as Rows however they di↵er slightly in the modifier methods that they provide due to the underlying system. Just as rows, headers, and footers can also be configured to use custom 19
public class CustomCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.backgroundColor = .darkGray textLabel?.textColor = .white } } ctx
instruction is encountered the current section will be ended, if a row is added after a new section will be automatically created and the following rows will be included in that section. ctx
Figure 3: Overview architecture of TableRender into di↵erent contexts can be made arbitrarily as long as it is done in the right order i.e. a section can span multiple table contexts. The architecture of the table context makes it easy to split up a list into multiple modules and later on compose them together without any coupling. 3.2.2 Parser The parser will take a table context and compute useful properties based on the contents of the context. It is the job of the parser to turn the one-dimensional data structure that the context is into a two-dimensional data structure with sections that can have a header, a footer, and multiple rows. The parser also extracts all views and reuse identifiers that are needed to render the list, these views are later registered in the UITableView. 22
let listContext = new TableContext() let Weekdays = new TableContext() weekdays
imperative methods that indicate the change, the old backing data is replaced with the new data. Certain combinations of updates will cause the table view to crash, for example, a reload and insert command on the same row will cause a crash, to mitigate this the reconciler has to split up the reconciler phase into smaller pieces and apply the changes in multiple batch updates and apply them incrementally. The diffing and reconciliation algorithm that is used can easily be changed to another implementation, the default implementation is using Di↵erenceKit [3]. 4 Discussion and Evaluation The thesis implements an approach to improve the developer experience for de- velopers developing complex dynamic scrolling user interfaces on mobile devices. This chapter will from the application developers’ point of view, evaluate the solu- tion that was described in the results. The project requirements that were posed on the project in the introduction will be used as the base of discussion. The provided solution (TableRenderer) will be compared to the traditional imperative modes of programming (UIKit). Based on Figure 1 - overview architecture of TableRender, we present the following code example. 4.1 Composability The library provides a way of splitting up lists into smaller parts and later combin- ing the parts into the final list. The composition can be used to split up a feature into pieces where di↵erent teams are responsible for di↵erent pieces or to organize code for maintainability and re-usability. The composition is achieved through functions, where each function creates a new TableContext object. The callee can use the returned context and merge it with its context via the infix operator. The 24
let tableView = UITableView() let tableRender = TableRender(tableView: tableView) func renderTable() { tableRender.render { $0
func Artists(model: [Artist]) -> TableContext { var context = TableContext() for artist in model { context TableContext { var context = TableContext() for song in model { context
of this library is designed to be more declarative than using the platforms’ native APIs. By the definition of declarative code above we can conclude that more declarative code can be measured by observing how much of the code is describing how the user interface should be constructed respectively how much of the code is describing what the user interface should look like. The main part of writing a list with TableRender is to implement a render function. The render function (see Figure 3) is the complete description of how the list should be rendered. The application developer does not have to consider the lists of previous states. The only way to modify the list is to call the render method with a completely new description of how the list should look like. This will result in a highly declarative code that does not have to consider insertions, deletions, registrations, and disposals. 4.3 Type Safety When creating a raw UITableView the splitting of the registration of views and the binding of views create an implicit relationship that is not covered by the type system instead the relationship is defined by opaque strings namely reuse iden- tifiers. The method that is used to dequeue a new cell dequeueReusableCell() is always returning a UITableViewCell that needs to be cast into the actual imple- mentation of the cell, this is not a type-safe operation. If the application developer changes the string in one place and not the other or if the developer forgets to register the cell the error would be caught at run time. In TableRender no such implicit relationship exists which enables the API to be fully type-safe. The library also solves another common adjacent problem that often occurs when implement- ing table views: invalid access to arrays. In the native APIs, the user does not configure individual rows to customize them instead the user overrides methods that get index paths as arguments and can return in di↵erent ways to provide 27
the correct behaviour. When implementing these methods array lookups are often used to know what behaviour should apply for that specific row. Invalid array look-ups are common errors that developers using the native table view API often stumble upon, in TableRender code the developer links the user interface to the data directly often by the use of a for therefore no array lookups are required thus eliminating a whole set of bugs. let item = data[indexPath.section].rows[indexPath.row] let cell = tableView.dequeueReusableCell( withIdentifier: "song-cell", for: indexPath) as! ArtistCell cell.textLabel?.text = name return cell Listing 16: Common implementation of a method that provides cells in the table view data source. Note the unsafe typecasting on line 4 and the risk of invalid access to arrays on line 1. 4.4 Coupling The level of coupling can be explored by asking the question: ”How much of one module must be known in order to understand another module?” [26]. When a system is built in a way so that the modules are loosely coupled each module can be understood in separation. One goal of the TableRender library is to decouple rows, headers, and footers from the table view implementation. The component-oriented API provided by TableRender lets each row to be independently implemented without considering the rest of the table. The decoupling allows teams to work on di↵erent components without interference, it also allows for greater reuse due to the component not being coupled to a specific list. 28
4.5 Cohesion The library aims to provide a way for developers to write more cohesive code when developing lists. In traditional methods provided by the platform, it is common to group the construction of a list into two stages: creation and binding. The two are there because the table view is reusing rows to increase the efficiency of the application. The problem with this solution is that the definition of a list item is divided into two separate locations in the code. In computer programming, the aim is usually to have a high level of cohesion, which is to say dependent code should live close to each other in the same module [5]. Another way to define cohesion is that code that is usually modified together should be close to each other. Splitting up code for a single list item results in low cohesion using both of these definitions of cohesion. This project provides an API where a row, header, and footer is always defined in a single place which in most cases will result in higher cohesion. 4.5.1 Example: UIKit Implementation Figure 4: Implementation of a list with di↵erent cell-types using pure UIKit. The definition of both of the cells are in this case split up in two methods: cellForRow() and didSelectRow(). The cellForRow() method is used by the table 29
view to obtain a view to render and is therefore required while the didSelectRow() is an optional override-able method on the delegate. Due to the configuration API being focused on the whole list it is hard to work on a single cell-type in isolation - this can make it hard for di↵erent teams to own cell-types on the same list. 4.5.2 Example: TableRender implementation Figure 5: Implementation of the same list from Figure 4 using TableRender. All the configurations of the di↵erent cells are specified in the same place. The onRender() method is similar to the cellForRow() method in the UIKit implementa- tion, however each cell-type needs to specify its own onRender() method unlike the per-list approach that pure UIKit-lists use. The didSelect() method is mapped directly to the didSelectRow() method on UIKit but is as all other configuration options on TableRender configured per cell-type, this can be seen as an implemen- tation of a component-oriented architecture [2]. The component-oriented model allows for lower coupling and stronger cohesion. 30
4.6 Mixed Lists In modern applications, lists with many di↵erent cell-types are a common feature. Di↵erent cell-types can be mixed in the same section or more commonly split up inside di↵erent sections. To make use of the reuse mechanism in place on table views, separate queues for di↵erent cell-types are needed. To implement this with raw table views the developer would utilize multiple di↵erent reuse identifiers [13]. Further many of the methods overridden by the delegate would also need to consider the di↵erent cell-types to provide the correct configuration, this often leads to conditional code in these methods that are hard to reason about and not particularly cohesive. In TableRenderer the reuse identifiers are automatically inferred based on the identity of the class that the cell is instantiated from however the application developer can choose to override the reuse identifier if, for example, a single class can produce two di↵erent types of cells. 31
4.6.1 Example: Mixed lists Suppose that we start with the following data model and want to create a list that wants to perform di↵erent actions when the user presses the artist and song rows. We also want the ability to customize the behaviour of the song and artist rows di↵erently (Listing 17). enum Cell: Hashable { case Artist(name: String) case Song(title: String) } struct Section: Hashable { let title: String let rows: [Cell] } let data: [Section] = [ Section( title: "Artist and Songs I Like", rows: [.Artist(name: "David Bowie"), ...] ), ... ] Listing 17: Example data model 4.6.2 Example: Mixed list with UIKit In Listing 18 we can see that the specification of how the two table cells should behave is spread out in di↵erent parts of the code. On lines 5 - 9 we start by registering the cells to the table view, this will allow us to later use the reuse- queue provided by UITableView. On lines 14 - 19 we handle the select event: here we need to retrieve the item from the data to know what type of item it is and then perform the action we want it to perform. On lines 21-38 the rows are dequeued and configured with the data that they should represent. 32
1 class ArtistCell: UITableViewCell { ... } 2 class SongCell: UITableViewCell { ... } 3 4 class TableViewController: UITableViewController { 5 func viewDidLoad() { 6 super.viewDidLoad() 7 self.tableView.register(SongCell.self, forCellReuseIdentifier: "song") 8 self.tableView.register(ArtistCell.self, forCellReuseIdentifier: "artist") 9 } 10 func numberOfSections(in tableView: UITableView) -> Int { return data.count } 11 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 12 return data[section].rows.count 13 } 14 func tableView( _ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 15 let item = data[indexPath.section].rows[indexPath.row] 16 switch item { 17 case .Artist(let name): print("pressed on artist " + name) 18 case .Song(let title): print("pressed on song " + title) 19 } 20 } 21 func tableView(_ tableView: UITableView, cellForRowAt ip: IndexPath) -> UITableViewCell { 22 let item = data[ip.section].rows[ip.row] 23 switch item { 24 case .Artist(let name): 25 let cell = tableView.dequeueReusableCell( 26 withIdentifier: "artist", 27 for: indexPath) as! ArtistCell 28 cell.textLabel?.text = name 29 return cell 30 case .Song(let title): 31 let cell = tableView.dequeueReusableCell( 32 withIdentifier: "song", 33 for: indexPath) as! SongCell 34 cell.textLabel?.text = title 35 return cell 36 } 37 } 38 } Listing 18: Example: An implementation using the pure UITableView implemen- tation. 33
4.6.3 Example: Mixed list with TableRender The same functionality could be implemented by using TableRender in the follow- ing way: 1 class ViewController: UITableViewController { 2 var tableBinder: TableRender! 3 override func viewDidLoad() { 4 tableBinder = TableRender(tableView: tableView) 5 6 tableBinder.render { ctx in 7 for section in data { 8 ctx
to a function. Another detail to notice is that no opaque strings are required to dequeue table cells, the user of the library does not have to consider registration, dequeuing, and disposal cells at all. 4.7 Flexibility and Extension Points Table Render allows for easy access to common table view API’s. When using the platform native API’s the developer implements delegates on a per list basis that handles events and styling. This means that the configuration methods are the same for all rows. Individual styling and event handling can be implemented by using the index path that the configuration method was called with and with that information decide how the specific row should behave. Instead of implementing delegates and handling events and styling on a per list level, Table Render allows the developer to specify callbacks and customise appearance on a per-cell level. All customization options that are traditionally configured trough UITableViewDelegate and UITableViewDataSource are speci- fied trough methods on the cell. The class that will produce a view can also be provided by the user to create rows with custom layouts. The API allows for all aspects of a cell to be specified in the same place. 4.8 Data updates A common feature of lists in modern applications is that the data is dynamic i.e the contents of the list change over time. The library makes it easier for the application programmer to develop lists that need to update based on data changes. When developing lists with raw UITableViews the application developer needs to know how the data changed from the previous dataset and notify the table view based on that. To make it easier for the developer table renderer provides a solution that handles diffing and reconciliation internally and let the developer 35
tableRender.render { context in context
struct ContentView: View { var body: some View { List { Section(header: Text("Important tasks")) { TaskRow() TaskRow() TaskRow() } Section(header: Text("Other tasks")) { TaskRow().onPress { alert() } TaskRow() TaskRow() } } } } Listing 21: Example of a list with two sections built with SwiftUI 5.1.1 Lists SwiftUI features a List component that can be used to create a list with sec- tions. The SwiftUI API is based on closures and function builders. The way that TableRenderer handles configuration via builder-pattern like methods on the rows are inspired by Swift UI. In the example above we can see how to set up an onPress handler which is similar to the way that the same action is done in TableRenderer. 5.2 React React is a library initially developed at Facebook that later became open-sourced [21]. React was created for use in web-development but since its inception, the library has evolved to operate on other platforms as well. The library aims to 37
provide a declarative and component-oriented application programming interface for developing user interfaces. To build user interfaces with react, the developer constructs small components which are independent reusable pieces of the user interface. The smaller components like buttons, text fields, and headers can then be composed into larger components, and these components can be composed into even larger components and so forth ultimately forming an application. These components form a tree, usually with smaller components in the nodes and larger components near the root. To structure changes in requirements based on user interaction and other events, user interfaces written with React utilize a one-way data flow i.e. data is passed down through the tree while events bubble up through the tree, every component in the tree can hold state but it can only be passed to that components children’s, the parent can only receive events from the child nodes. TableRender is like many other libraries developed to provide declarative user interfaces somewhat inspired by the React way of thinking, although this project is much more specialized both in scope and execution while the React project is highly generic. 5.3 Cycler Cycler is an open-source library developed by Square [23]. The goal of this project and Cycler is in many ways similar. The library acts as a wrapper around Androids platforms RecyclerView and provides a declarative API for application developers. The library in its authors’ words claims that it ”allows you to easily configure an Android RecyclerView declaratively in a succinct way”. While this project and Cycler share many of the same goals the structure of cycler di↵ers from that of this project. Cycler works by first creating the definition of every type of cell. Each definition has a method forItemsWhere which decides what cell a particular 38
row should render, that gets called for each row in the list. The data are provided later, this is the main di↵erence between cycler and TableRender. In table render the data and definition of the cell are provided at the same time, this means that no mapping function has to be provided. 5.4 Di↵able Data Source In iOS 13 Apple released a new method of managing the data source on table and collection views. UITableViewDi↵ableDataSource replaces the UITableViewData- Source and comes with a few new features that make it easier for the application developer to update the underlying data and have it synced to the user interface. Instead of having to calculate the changes in data and calling methods on the table view based on that information the di↵able data source is utilizing a more declarative approach. To apply an update with a di↵able data source the developer creates an empty snapshot. The snapshot is then filled with the desired sections and rows and can later be applied to the table. When the snapshot is applied to the table view the platform will di↵ and reconcile the user interface with the new snapshot. This technique requires the developer to not have to reason about the previous state of the list. This is similar to the way that TableContext works in TableRenderer. Di↵able data sources are only available in iOS 13 and later. 5.5 Litho Litho is a framework developed at Facebook for building efficient Android user interfaces in a declarative way [20]. The framework was built to implement complex scrolling user interfaces. Litho provides a declarative API similar to that of React. Litho provides interfaces for building lists that are called Sections. The problems that Sections are aiming to solve are complex lists with multiple view types, data sources, and nesting of lists. Sections are an abstraction over Androids Recycle 39
You can also read