LouisDotCom / NextItem

what to do!
0 stars 1 forks source link

Move the items array to a plist file #5

Open cristoper opened 7 years ago

cristoper commented 7 years ago

Following the Model-View-Controller paradigm, your model is currently an Array of Strings which is hardcoded with the item text. As a first step to allowing users to add/edit items, let's replace the Array with a model object which can read the list of items from a file on disk.

(I think you've already implemented this for the most part, but hopefully some of the description and references below are helpful.)

Create plist and add to Xcode

I like your idea of storing the list of items in a property list; it is simple and allows adding attributes (like a "priority" to weight the frequency) to items in the future. I created a plist from within Xcode (File > New > File... > choose the "Property List" template). That automatically adds it to the Project (which in turn gets it copied to the application's bundle during build time). Using Xcode's plist editor I added an Array of Strings and copied-pasted all of your items into it.

I know you've already converted your array to a plist using a Python script (and I think you've also successfully added it to your Xcode Project), so that's done.

Create new type to manage our plist

A modular way to implement our new model will be to define a new type of object which will manage loading the item list from the plist file (and in the future will handle saving the modified list back to a file).

In Swift, custom object types can be defined as either Structs or as Classes. Both are very similar, the difference is how they behave when you assign them to a variable or pass them as an argument to a function:

If you are not sure whether to use a Struct or a Class, the safer thing is to use a Struct.

It is usual to define types in their own files (File > New > File... > "Swift File"). I named my implementation ItemArray.

For now, the ItemArray Struct needs only two basic methods: a way to initialize it with the name of a plist file from which it will read the list of items, and a way to ask it for a random item from the list. Here's a skeleton implementation:

struct ItemArray {

    // This is a stored property (whose type is an Array of Strings) which will store the
    // list of items in memory once they are read from the plist file
    var items: [String] = []

    // This is the initializer; it takes an optional parameter "plistName" which defaults to "items"
    init (plistName name: String = "items") {

    // TODO: find plist in main bundle by its name and load the file into memory
    // TODO: parse the plist file into an array

    }

    // This method returns a random item
    func randomItem() -> String {
        // TODO: select random item, return its text as a String
    }
}

Further reading on Structs and Classes in Swift:

Read file from main bundle

By default, Xcode will copy resources (like our plist file) into the main bundle when it builds the application. The Bundle.main.path(forResource:ofType:) method can be used to get the path to a file with the given name and extension:

let path = Bundle.main.path(forResource: name, ofType: "plist")!

The return type of the path() method is String?, an optional String (an optional type can be nil meaning it has no value). The exclamation mark at the end of the line above explicitly unwraps an optional and guarantees that the path variable will be a non-optional String, but if the path() method does return nil then the program will crash. See also: The Swift Programming Language: Optionals

Once we have a path to the plist, we need to read it into memory using the FileManager class:

let data = FileManager.default.contents(atPath: path)

Parse plist data into Array

Once we have the plist in memory, we can use the PropertyListSerialization class to parse the data into a Swift Array:

do {
    try items = PropertyListSerialization.propertyList(
        from: data!,
        options:PropertyListSerialization.MutabilityOptions.mutableContainers,
        format: nil) as! [String]
} catch let e as NSError {
    print(e.localizedDescription)
}

The propertyList() method which does the parsing will throw an exception if it encounters an error, so we have to put it in a do-catch statement to handle any errors (in this case we just print any error to the console).

Further reading:

Random item

The implementation of randomItem() can re-use the code you are currently using in your view controller:

func randomItem() -> String {
     let numItems = UInt32(items.count)
     let randomIndex = Int(arc4random_uniform(numItems))

     return items[randomIndex]
    }

Use ItemArray object from view controller

Once you've defined an ItemArray struct, you'll want to delete the hardcoded Items array from ViewController.swift and use ItemArray instead. In my implementation I added a property called items to the view controller:

var items: ItemArray? = nil

It is an optional initialized to nil, but then I assign it to an instance of ItemArray in the view controller's viewDidLoad() method:

items = ItemArray(plistName: "items")

And then in the NextItemControl IBAction which is called whenever the button is tapped, call the randomItem() method:

@IBAction func NextItemControl(_ sender: UIButton) {
    // Display a random item
    if let randomItem = items?.randomItem() {
        TextViewItem.text=("¡ \(randomItem) !")
    }
}

In the above snippet we use a Swift feature called Optional Chaining which calls randomItem() only if items is not nil (remember we defined it as an optional).

My implementation

I've committed my full implementation of ItemArray into a branch called file-model in my clone of your git repository. You can browse it on GitHub: https://github.com/cristoper/NextItem/tree/file-model

Or you can clone my repo, checkout the file-model branch, and try running it in Xcode's simulator:

$ git clone https://github.com/cristoper/NextItem.git chris-nextitem
$ cd chris-nextitem
$ git checkout file-model
$ open NextItem.xcodeproj/
LouisDotCom commented 6 years ago

Hi, Chris. I'm finally back to this project. I followed most of what you wrote in this email. As a reminder, between the time you saw my first version and did this work outlined below (and applied changes to a branch of NextItem), I had already created a version with an editable array, using two viewcontrollers. So here's where things stand (and I'm stuck): although I updated most of my code...

However, when I compile, the Main.storyboard is still referencing SecondViewController, so the build crashes, not surprisingly.

So, I followed these steps, thinking I'd just copy your Main.storyboard:

$ git clone https://github.com/cristoper/NextItem.git chris-nextitem $ git cd chris-nextitem $ git checkout file-model $ open NextItem.xcodeproj/

But this version fails, too, when I attempt to run it on a simulator. I noticed it didn't include the items.plist so I added that file, but where the ViewController refers to "ItemArray" there's an error 2x: Use of undeclared type "ItemArray"

Let me know if you want to do a remote session (or maybe just run that version on your computer and see if it also fails)

Did you get a server?

tnx - Louis

On Fri, Aug 4, 2017 at 5:05 PM, chris burkhardt notifications@github.com wrote:

Following the Model-View-Controller paradigm, your model is currently an Array of Strings which is hardcoded with the item text. As a first step to allowing users to add/edit items, let's replace the Array with a model object which can read the list of items from a file on disk.

(I think you've already implemented this for the most part, but hopefully some of the description and references below are helpful.) Create plist and add to Xcode

I like your idea of storing the list of items in a property list; it is simple and allows adding attributes (like a "priority" to weight the frequency) to items in the future. I created a plist from within Xcode (File > New > File... > choose the "Property List" template). That automatically adds it to the Project (which in turn gets it copied to the application's bundle during build time). Using Xcode's plist editor I added an Array of Strings and copied-pasted all of your items into it.

I know you've already converted your array to a plist using a Python script (and I think you've also successfully added it to your Xcode Project), so that's done. Create new type to manage our plist

A modular way to implement our new model will be to define a new type of object which will manage loading the item list from the plist file (and in the future will handle saving the modified list back to a file).

In Swift, custom object types can be defined as either Structs or as Classes. Both are very similar, the difference is how they behave when you assign them to a variable or pass them as an argument to a function:

  • Structs define what are called "value" types in Swift's vocabulary: every time a value type is assigned to a variable, the variable points to its own copy of the type. Changing a property in one variable will not change the property in the others.
  • Classes define "reference" types: every time a reference type is assigned to a variable, the variables all point to the same copy of the object. Changing a property in one variable will change it for all variables.

If you are not sure whether to use a Struct or a Class, the safer thing is to use a Struct.

It is usual to define types in their own files (File > New > File... > "Swift File"). I named my implementation ItemArray.

For now, the ItemArray Struct needs only two basic methods: a way to initialize it with the name of a plist file from which it will read the list of items, and a way to ask it for a random item from the list. Here's a skeleton implementation:

struct ItemArray {

// This is a stored property (whose type is an Array of Strings) which will store the    // list of items in memory once they are read from the plist file    var items: [String] = []

// This is the initializer; it takes an optional parameter "plistName" which defaults to "items"    init (plistName name: String = "items") {

// TODO: find plist in main bundle by its name and load the file into memory    // TODO: parse the plist file into an array
}

// This method returns a random item    func randomItem() -> String {
    // TODO: select random item, return its text as a String    }

}

Further reading on Structs and Classes in Swift:

Read file from main bundle

By default, Xcode will copy resources (like our plist file) into the main bundle https://developer.apple.com/documentation/foundation/bundle when it builds the application. The Bundle.main.path(forResource:ofType:) method can be used to get the path to a file with the given name and extension:

let path = Bundle.main.path(forResource: name, ofType: "plist")!

The return type of the path() method is String?, an optional String (an optional type can be nil meaning it has no value). The exclamation mark at the end of the line above explicitly unwraps an optional and guarantees that the path variable will be a non-optional String, but if the path() method does return nil then the program will crash. See also: The Swift Programming Language: Optionals https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html#//apple_ref/doc/uid/TP40014097-CH5-ID330

Once we have a path to the plist, we need to read it into memory using the FileManager https://developer.apple.com/documentation/foundation/filemanager class:

let data = FileManager.default.contents(atPath: path) Parse plist data into Array

Once we have the plist in memory, we can use the PropertyListSerialization https://developer.apple.com/documentation/foundation/propertylistserialization class to parse the data into a Swift Array:

do { try items = PropertyListSerialization.propertyList( from: data!, options:PropertyListSerialization.MutabilityOptions.mutableContainers, format: nil) as! [String] } catch let e as NSError { print(e.localizedDescription) }

The propertyList() method which does the parsing will throw an exception https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html if it encounters an error, so we have to put it in a do-catch statement to handle any errors (in this case we just print any error to the console).

Further reading:

Random item

The implementation of randomItem() can re-use the code you are currently using in your view controller:

func randomItem() -> String { let numItems = UInt32(items.count) let randomIndex = Int(arc4random_uniform(numItems))

 return items[randomIndex]
}

Use ItemArray object from view controller

Once you've defined an ItemArray struct, you'll want to delete the hardcoded Items array from ViewController.swift and use ItemArray instead. In my implementation I added a property called items to the view controller:

var items: ItemArray? = nil

It is an optional initialized to nil, but then I assign it to an instance of ItemArray in the view controller's viewDidLoad() method:

items = ItemArray(plistName: "items")

And then in the NextItemControl IBAction which is called whenever the button is tapped, call the randomItem() method:

@IBAction func NextItemControl(_ sender: UIButton) { // Display a random item if let randomItem = items?.randomItem() { TextViewItem.text=("¡ (randomItem) !") } }

In the above snippet we use a Swift feature called Optional Chaining https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/OptionalChaining.html which calls randomItem() only if items is not nil (remember we defined it as an optional). My implementation

I've committed my full implementation of ItemArray into a branch called file-model in my clone of your git repository. You can browse it on GitHub: https://github.com/cristoper/NextItem/tree/file-model

Or you can clone my repo, checkout the file-model branch, and try running it in Xcode's simulator:

$ git clone https://github.com/cristoper/NextItem.git chris-nextitem $ git cd chris-nextitem $ git checkout file-model $ open NextItem.xcodeproj/

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/LouisDotCom/NextItem/issues/5, or mute the thread https://github.com/notifications/unsubscribe-auth/AHFJsfC0UQqmdRZnAVHN98IsNvMeTbXRks5sU6PRgaJpZM4OuReh .