2026-02-02 21:55:12 +01:00

265 lines
12 KiB
Plaintext

@Tutorial {
@Intro(title: "The task list view") {
Designing a user interface is a complex task.
The core feature of the _Subtasks_ app is breaking down complex tasks into smaller subtasks, which is very helpful when creating UIs.
In this first tutorial about designing the UI, you'll create the task list view.
}
@Section(title: "Create a new view type") {
@ContentAndMedia {
It is common to strictly separate type definitions from the UI definition.
Therefore, you'll create a new file containing a new type.
}
@Steps {
@Step {
Create a new file `TaskList.swift` in the `Sources` folder and create the structure `TaskList`.
@Code(name: "TaskList.swift", file: "TaskList1.swift")
}
@Step {
Make this structure conform to ``View`` by adding a placeholder UI.
Now, it can be implemented in other views as a view.
@Code(name: "TaskList.swift", file: "TaskList2.swift")
}
@Step {
The view displays an array of tasks. The information has to be passed to the structure from its parent view.
@Code(name: "TaskList.swift", file: "TaskList3.swift")
}
@Step {
``Binding`` enables the view to modify the data it gets from the parent view.
This is necessary in this case as the view enables to change the completion of the tasks.
@Code(name: "TaskList.swift", file: "TaskList4.swift")
}
}
}
@Section(title: "Preview the view") {
@ContentAndMedia {
You want to regularly test the appearance of your UI while developing.
Add the view to the window in order to observe changes when running the app.
}
@Steps {
@Step {
Open the `Subtasks.swift` file.
@Code(name: "Subtasks.swift", file: "Subtasks2.swift", previousFile: "Subtasks1.swift")
}
@Step {
Add example data as the app's state.
@Code(name: "Subtasks.swift", file: "Subtasks3.swift")
}
@Step {
Replace the current text view by the task list view and run the app for previewing.
When you edit the code, it is recommended to run the app in order to test the changes.
@Code(name: "Subtasks.swift", file: "Subtasks4.swift") {
@Image(source: "Subtasks4.png", alt: "The window.")
}
}
}
}
@Section(title: "Design the task list") {
@ContentAndMedia {
Discover how to use a list to present the available tasks.
}
@Steps {
@Step {
Open the `TaskList.swift` file.
@Code(name: "TaskList.swift", file: "TaskList4.swift") {
@Image(source: "Subtasks4.png", alt: "The window.")
}
}
@Step {
Replace the text view with a list showing the labels of the tasks.
The ``List`` renders its content for each element in the tasks array.
@Code(name: "TaskList.swift", file: "TaskList5.swift") {
@Image(source: "TaskList5.png", alt: "The window.")
}
}
@Step {
Wrap the list with a ``ScrollView``.
Lists grow as items are added, and can exceed the window's height.
The scroll view enables a part of the list to be hidden and shown with the scrolling gesture.
@Code(name: "TaskList.swift", file: "TaskList6.swift") {
@Image(source: "TaskList5.png", alt: "The window.")
}
}
@Step {
An often used style for lists is "boxed-list". There are specialized widgets with the suffix "Row" that are designed to be used inside a boxed list.
Implement this style in the task list view.
A reference of the available styles is available in the [libadwaita docs](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/style-classes.html).
@Code(name: "TaskList.swift", file: "TaskList7.swift") {
@Image(source: "TaskList7.png", alt: "The window.")
}
}
@Step {
You can see that the list expands to fill the whole scroll view.
It is possible to fix this by stating explicitly that the list should be aligned to the start of the scroll view.
@Code(name: "TaskList.swift", file: "TaskList8.swift") {
@Image(source: "TaskList8.png", alt: "The window.")
}
}
@Step {
Add the check button to the beginning of the row.
On line 19, you can see that it is possible to directly get an element from an array as a binding when providing the identifier or index.
@Code(name: "TaskList.swift", file: "TaskList9.swift") {
@Image(source: "TaskList9.png", alt: "The window.")
}
}
@Step {
In this app, a bit more padding around the list view and restricting the maximum width can make the UI fit better into the GNOME desktop.
@Code(name: "TaskList.swift", file: "TaskList10.swift") {
@Image(source: "TaskList10.png", alt: "The window.")
}
}
@Step {
Add icons to indicate that one can navigate to the subtasks by pressing on the row, and make rows not selectable so that they feel more like buttons.
It is recommended to use the ``Icon`` type for default icons.
@Code(name: "TaskList.swift", file: "TaskList11.swift") {
@Image(source: "TaskList11.png", alt: "The window.")
}
}
}
}
@Section(title: "Add support for creating new tasks") {
@ContentAndMedia {
Explore dialogs while creating the essential feature to add new tasks.
}
@Steps {
@Step {
Delete the file `Sources/ToolbarView.swift`.
@Image(source: "DeleteFile.png", alt: "GNOME Builder's file context menu in the sidebar.")
}
@Step {
Remove the top toolbar in the file `Subtasks.swift`.
@Code(name: "Subtasks.swift", file: "Subtasks5.swift", previousFile: "Subtasks4.swift") {
@Image(source: "Subtasks5.png", alt: "The window.")
}
}
@Step {
Open the `TaskList.swift` file.
@Code(name: "TaskList.swift", file: "TaskList11.swift") {
@Image(source: "Subtasks5.png", alt: "The window.")
}
}
@Step {
Add a top toolbar containing an empty header bar around the scroll view.
@Code(name: "TaskList.swift", file: "TaskList12.swift") {
@Image(source: "TaskList12.png", alt: "The window.")
}
}
@Step {
Move the list definition to another computer variable, making the code more readable.
@Code(name: "TaskList.swift", file: "TaskList13.swift") {
@Image(source: "TaskList12.png", alt: "The window.")
}
}
@Step {
Instead of computed variables, it's possible to use functions if parameters are needed.
Extract the row into a function.
@Code(name: "TaskList.swift", file: "TaskList14.swift") {
@Image(source: "TaskList12.png", alt: "The window.")
}
}
@Step {
Add a plus button to the start of the toolbar, opening the dialog that will be implemented in the next step.
@Code(name: "TaskList.swift", file: "TaskList15.swift") {
@Image(source: "TaskList15.png", alt: "The window.")
}
}
@Step {
Add the dialog. It will be presented when `showAddDialog` is true.
Depending on the window size, you will either see a bottom sheet as in the screenshot or a floating window when pressing the plus button.
@Code(name: "TaskList.swift", file: "TaskList16.swift") {
@Image(source: "TaskList16.png", alt: "The window.")
}
}
@Step {
Make an entry row for the new task's label the content of the dialog.
@Code(name: "TaskList.swift", file: "TaskList17.swift") {
@Image(source: "TaskList17.png", alt: "The window.")
}
}
@Step {
Replace the close button in the dialog's toolbar with two buttons.
A concept you haven't seen so far are signals conforming to the ``Signal`` type. When sending a signal (see line 23), the actions the signal is connected to (line 46, focusing the entry row) are executed.
@Code(name: "TaskList.swift", file: "TaskList18.swift") {
@Image(source: "TaskList18.png", alt: "The window.")
}
}
}
}
@Section(title: "Add keyboard shortcuts") {
@ContentAndMedia {
Adding keyboard shortcuts can significantly enhance navigation via the keyboard only which works already good without explicitly setting shortcuts.
}
@Steps {
@Step {
Add a keyboard shortcut for adding a task to the text entry.
The ``EntryRow/entryActivated(_:)`` modifier adds a function that is executed when the enter key is pressed inside the text entry.
@Code(name: "TaskList.swift", file: "TaskList19.swift", previousFile: "TaskList18.swift") {
@Image(source: "TaskList18.png", alt: "The window.")
}
}
@Step {
Add a keyboard shortcut for opening the dialog using the ``Button/keyboardShortcut(_:app:)`` modifier.
The code will fail to build as you have added a new stored property to the structure that has to be passed to the structure as a parameter.
@Code(name: "TaskList.swift", file: "TaskList20.swift")
}
@Step {
Update the `Subtasks.swift` file so that the app builds again. Then, run the app and press `Ctrl+n` to test the shortcut.
@Code(name: "Subtasks.swift", file: "Subtasks6.swift", previousFile: "Subtasks5.swift") {
@Image(source: "TaskList18.png", alt: "The window.")
}
}
@Step {
``Binding/onSet(_:)`` lets you observe a binding. Reset the text whenever the dialog is closed using the escape key or by swiping down in the bottom sheet view.
@Code(name: "TaskList.swift", file: "TaskList21.swift", previousFile: "TaskList20.swift") {
@Image(source: "TaskList18.png", alt: "The window.")
}
}
}
}
@Section(title: "Localize the strings") {
@ContentAndMedia {
In this tutorial so far, we have simply used Swift string literals for labels of UI elements.
While this might work if the UI is available in English only, it makes sense to use a more adaptive solution.
}
@Steps {
@Step {
Open the file `Localized.yml`, and delete its content except for the first line that sets the fallback language to English.
@Code(name: "Localized.yml", file: "Localized1.yml")
}
@Step {
Then, add translations for all the languages you want to support. Follow the instructions in the [Localized docs](https://git.aparoksha.dev/aparoksha/localized#usage).
@Code(name: "Localized.yml", file: "Localized2.yml")
}
@Step {
Update the UI to use the localized strings, and add a tooltip to the plus button.
@Code(name: "TaskList.swift", file: "TaskList22.swift", previousFile: "TaskList21.swift")
}
}
}
}