I have rewritten the first version of the Directory program as a SwiftUI app.
The first view in the app displays a list of people.
Clicking the + button in the navigation bar takes the user to a second view where they can enter details for a new person.
This app will feature the use of a model class that will be responsible for storing the app's data.
Individual entries in the directory are represented by Person objects.
class Person : Codable, Identifiable { var id = UUID() var name : String var office : String init(name n : String, office o : String) { name = n office = o } }
To make it possible to display Person objects in a SwiftUI list, we need to meet the requirement that each object have a unique id. We meet this requirement by having our Person class implement the Identifiable protocol and by giving our Person class an id property. The UUID() function generates a unique identifier for each person on demand.
The data model class for our application is the Directory class. A Directory stores a list of Person objects and includes methods for reading and writing the app's data to a property list file.
class Directory : ObservableObject { @Published var people : [Person] @Published var newPerson : Person = Person(name:"",office:"") let itemArchiveURL: URL = { let documentsDirectories = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let documentDirectory = documentsDirectories.first! return documentDirectory.appendingPathComponent("directory.plist") }() init() { do { let data = try Data(contentsOf: itemArchiveURL) let unarchiver = PropertyListDecoder() let loc = try unarchiver.decode(Array<Person>.self, from: data) people = loc } catch { people = [Person(name:"Acacia Ackles",office:"Steitz 131"), Person(name:"Joe Gregg",office:"Briggs 413"), Person(name:"Kurt Krebsbach",office:"Briggs 411") ] } } func makeNewPerson() { newPerson = Person(name:"",office:"") people.append(newPerson) } func delete(at offsets: IndexSet) { people.remove(atOffsets: offsets) } func saveChanges() { do { let encoder = PropertyListEncoder() let data = try encoder.encode(people) try data.write(to: itemArchiveURL) } catch { } } }
In examples up to this point we have seen that views make use of @State
variables. A special characteristic of @State
variables is that they automatically watch for changes. Changing the value of a @State
variable causes the view containing the variable to be redrawn.
When we place our Directory data model object in a view we will no longer be using @State with the property that stores this object. Instead, we are going to use an alternative approach:
@Published
. Like @State
properties, @Published
properties are observed by SwiftUI. Changes to @Published
properties will trigger redraws of views that depend on them.@ObservedObject
or @StateObject
property wrappers. This tells SwiftUI that the object in question contains @Published properties that the system needs to watch for changes.Here is the code for the first view in the app. This view sets up the navigation controller and the first view displayed in that controller. That first view displays the list of people in our directory.
struct ContentView: View { @StateObject var directory : Directory @State var isShowingDetailView = false var body: some View { NavigationView { VStack { NavigationLink(destination: PersonView(d:directory), isActive: $isShowingDetailView) { EmptyView() } List { ForEach(directory.people) { p in HStack { Text(p.name) Spacer() Text(p.office) } }.onDelete(perform:delete) } }.navigationBarItems(leading: EditButton(),trailing: Button("+"){ directory.makeNewPerson() isShowingDetailView = true; } ) } } func delete(at offsets: IndexSet) { directory.delete(at:offsets) directory.saveChanges() } }
To create a new entry in the directory the user will click the + button in the navigation bar. Normally we would use a NavigationLink to implement the + button. In this case we use a button instead. The reason for this is that a NavigationLink simply moves you to a new view: if you need to perform additional actions before doing the navigation you will need a button's action closure to do that.
To implement the navigation action in the + button we use this strategy.
Another feature that this view demonstrates is the delete gesture for list items. In the code that sets up the list items I have used the onDelete(perform:)
modifer. This will call our view's delete()
method when the user asks to delete a list item.
The second view allows the user to enter the details for a new person they have just created.
struct PersonView: View { @StateObject var d : Directory var body: some View { VStack { HStack { Text("Name:").frame(width:60,height:20,alignment: .trailing) TextField("",text:$d.newPerson.name).textFieldStyle(.roundedBorder) } HStack { Text("Office:").frame(width:60,height:20,alignment: .trailing) TextField("",text:$d.newPerson.office).textFieldStyle(.roundedBorder) } Button("Save Changes"){ d.saveChanges() }.buttonStyle(.borderedProminent).padding() } } }
The code for the + button in the first view calls the Directory's makeNewPerson()
method. This creates a new Person object and stores that new Person in the Directory's newPerson
property for easy access. This second view will access that newPerson
property and link its name and office properties to the two text fields where the user can enter that data.