This is the last of our series of tutorials, so let’s continue with WWDC2019’s Introducing SwiftUI: Building Your First App to learn how to use SwiftUI. This article will show you how to adjust views to user input in SwiftUI and how to use previews to see how our applications will look on different systems.

Download the project

Download the starting code for this tutorial by doing the following:

git clone https://github.com/swiftui-from-zero/wwdc2019_building_your_first_app.git
cd wwdc2019_building_your_first_app
git checkout before-mutable-list
Copy the code

Open wwdc2019_building_your_first_app/Room/ room.xcodeproj with Xcode.

Still hope you can follow the tutorial to write code together, so that learning efficiency is higher!

Extract storage model

Let’s start by explaining how the starting code differs from what we did at the end of last lecture. Compared to the end of (3), we mainly added the roomStore. swift file, along with some images to be used later.

The code in RoomStore.swift is very simple, just a class that stores an array of meeting rooms:

import SwiftUI

class RoomStore {
    var rooms: [Room]
    
    init(rooms: [Room] = []) {
        self.rooms = rooms
    }
}
Copy the code

We usually organize the storage model separately from the UI to keep the storage logic and UI logic separate for better application development. In this case, we plan to use the RoomStore class to store the data needed by the application, namely the list of meeting rooms. This change requires changing the [Room] parameter in ContentView to the RoomStore type. The changed code becomes:

struct ContentView: View {
    var store : RoomStore = RoomStore(a)Var rooms: [Room] = []

    var body: some View {
        NavigationView {
            List(store.rooms) { room in  // Change rooms to store.rooms
                RoomCell(room: room)
            }
            .navigationTitle(Text("Rooms"))}}}Copy the code

We also need to change the initialization mode of the preview section:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(store: RoomStore(rooms: testData))
    }
}
Copy the code

Adding and deleting conference rooms

Now our view can only display default values from testData. To make our app more useful, users need to be able to add and remove conference rooms.

First, add. For simplicity, we’re going to add a button that, when clicked, adds to the list a huge conference room called “Hall 2” that can seat 2,000 people.

Let’s change the view first by adding a button to the List. A feature of List is that it can dynamically generate a List based on the data passed in, that is, each element of the array passed in is converted into a grid. You can also generate a static list, where you just give it all the boxes and it arranges them. To add this button, we need to make the dynamic list static. That is, from:

List(store.rooms) { room in
    RoomCell(room: room)
}
Copy the code

To:

List {
    ForEach(store.rooms) { room in
        RoomCell(room: room)
    }
}
Copy the code

You can just think of this ForEach as a loop. These changes do not change how the preview looks. After static, we can add the button (if adding a button to the dynamic list, it is equivalent to adding a button to each conference room, interested friends can try) :

List {
    Button(action: {}) {
        Text("Add Room")}ForEach(store.rooms) { room in
        RoomCell(room: room)
    }
}
Copy the code

After adding the button to the view, we bind the action of clicking the button. Add an addRoom function to the ContentView as the button’s action argument. We’re going to add Hall 2 to the conference room at the click of a button. Then, recalling the Zoomed from the RoomDetail view in (3), we try to precede the store argument with an @state. So the code becomes:

struct ContentView: View {
    @State var store: RoomStore = RoomStore(a)var body: some View {
        NavigationView {
            List {
                Button(action: addRoom) {
                    Text("Add Room")}.
            }
            .}}func addRoom(a) {
        store.rooms.append(Room(name: "Hall 2", capacity: 2000))}}Copy the code

Try clicking the button in preview at this point, but nothing works. Well, why is that? Note here that our RoomStore is a reference type (class), not a value type (struct). SwiftUI requires us to do different things for value types and reference types. If you change class RoomStore to struct RoomStore in RoomStore.swift, the button becomes valid.

For the reference type, we need to modify the RoomStore class, add ObservableObject to it, and add @Published Property Wrapper to the member variable that the view needs to focus on.

class RoomStore: ObservableObject {
    @Published var rooms: [Room]
    
    init(rooms: [Room] = []) {
        self.rooms = rooms
    }
}
Copy the code

Also, we’re going to use @ObservedObject instead of @State in front of store in our ContentView.

struct ContentView: View {
    @ObservedObject var store: RoomStore = RoomStore(a)var body: some View {
        NavigationView {
            List {
                Button(action: addRoom) {
                    Text("Add Room")}.
            }
            .}}func addRoom(a) {
        store.rooms.append(Room(name: "Hall 2", capacity: 2000))}}Copy the code

With this adjustment, our buttons are ready to go.

Next, we simply changed the style of the list to separate the Add Room button from the conference Room displayed below. Use the. ListStyle modifier here. It’s going to group the list according to the sections that we’ve divided. The modified List is as follows:

List {
    // There are two sections here
    Section {
        Button(action: addRoom) {
            Text("Add Room")}}Section {
        ForEach(store.rooms) { room in
            RoomCell(room: room)
        }
    }
}
.navigationTitle(Text("Rooms"))
.listStyle(GroupedListStyle())
Copy the code

You can see that the list in the preview has changed accordingly.

Next, let’s add the delete operation. The delete operation is particularly simple, just use the onDelete modifier on the ForEach that generates the list unit. Of course, we also need to provide a callback function for onDelete. Write a delete function in the ContentView. Note that the input to this function should be IndexSet. Just pass this function to onDelete. The code for the modified ContentView is as follows:

struct ContentView: View {
    .

    var body: some View {
        NavigationView {
            List {
                .
                Section {
                    ForEach(store.rooms) { room in
                        RoomCell(room: room)
                    }
                    .onDelete(perform: delete)
                }
            }
            .}}.

    func delete(at offset: IndexSet) {
        store.rooms.remove(atOffsets: offset)
    }
}
Copy the code

When you run the preview after the code changes, SwiftUI automatically generates an animation for the deletions. Swipe left on the panel of the conference room you want to delete, and the red delete button will appear as iOS custom. Tap to delete.

At this point, we are done inserting and deleting list elements.

Add Edit Mode

Click the Edit button in the upper right corner to enter the edit mode. You can reorder or delete the elements in the list. After the operation is complete, click the button again to exit the edit mode and save the changes made during the process.

Yes, SwiftUI makes it easy to add such features. Just add a.navigationbaritems (trailing: EditButton()) to the List to get the EditButton in the upper right. For drag-and-drop sorting, add onMove to ForEach and a move function similar to delete above. The modified code is as follows:

struct ContentView: View {
    .

    var body: some View {
        NavigationView {
            List {
                .
                Section {
                    ForEach(store.rooms) { room in
                        RoomCell(room: room)
                    }
                    .onDelete(perform: delete)
                    .onMove(perform: move)
                }
            }
            .
            .navigationBarItems(trailing: EditButton())
            .}}.
    
    func delete(at offset: IndexSet) {
        store.rooms.remove(atOffsets: offset)
    }
    
    func move(from source: IndexSet.to destination: Int) {
        store.rooms.move(fromOffsets: source, toOffset: destination)
    }
}
Copy the code

Run the preview after modifying the code to get the look in the GIF above.

Add more previews for different environment Settings

At this point we are done with the conference room application! However, when users use the app, the system environment may be different: some users may use a larger default font size; Some users prefer dark mode. During the development process, we also need to ensure that these users can use our app properly. Is there any way to see what our app looks like under different environment Settings? Don’t worry, the engineers at SwiftUI have taken this into account and we can implement this feature by adding different previews.

It is the same as the page in (3) for checking whether there is a camera. We can add a preview to see what the application will look like on different systems. In the preview section of contentView.swfit. Hold command and click on ContentView and select Group to place the preview view in the Group. For large type, insert

ContentView(store: RoomStore(rooms: testData))
		.environment(\.sizeCategory, .extraExtraLarge)
Copy the code

The modified preview section is as follows:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView(store: RoomStore(rooms: testData))
            ContentView(store: RoomStore(rooms: testData))
                .environment(\.sizeCategory, .extraExtraLarge)
        }
    }
}
Copy the code

You can now see the view under the added large size on the right:

We can also continue to add a preview in dark mode. Join in the Group you just created

ContentView(store: RoomStore(rooms: testData))
		.environment(\.sizeCategory, .extraExtraLarge)
Copy the code

Change the preview part of the code to:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView(store: RoomStore(rooms: testData))
            ContentView(store: RoomStore(rooms: testData))
                .environment(\.sizeCategory, .extraExtraLarge)
            ContentView(store: RoomStore(rooms: testData))
                .environment(\.colorScheme, .dark)
        }
    }
}
Copy the code

We now have a preview in dark mode:

This concludes our tutorial. In this tutorial, we learned how to dynamically adjust data based on user input, how to add editing modes, and how to make better use of the preview feature to see what the application will look like in different environments. This is the last of a series of tutorials that will give you a general idea of how to use SwiftUI and how easy it is to write an app