BookStore
π
νκΈ λ²μ
See new releases and search for programming books from IT Bookstore API
This is a sample app to practice using Result
type, stubbing network request for unit tests, separating functionalities into frameworks, and writing Swift documentation.
How to run
> cd BookStore
> open BookStore.xcodeproj
Run!
Contents
App Features
What's New
A simple UITableView
with cells and modal presentation for a detailed page.
Search
-
As a user types in the keyword, the search text is "debounced" for a fraction of second for better performance and user experience. See Debouncer.
-
Search results are paginated and provides infinite scroll.
Result
type in Swift 5
Out of the box, you have to switch on the Result
instance to access the underlying success instance or the error instance.
switch result {
case .success(let response):
//do something with the response
case .failure(let error):
//handle error
}
However, I think switch statements are too wordy. I added success
and catch
method to Result
type. So it can be chained like this.
searchResult.success { response in
//do something with the response
}.catch { error in
//handle error
}
Even cleaner, like this.
result.success(handleSuccess)
.catch(handleError)
func handleSuccess(_ result: SearchResult) { ... }
func handleError(_ error: Error) { ... }
Stubbing Network Requests for Unit Tests
Generally, it is not a good idea to rely on the actual network requests for unit tests because it adds too much dependency on tests. One way to stub networking is to subclass URLProtocol
.
URLProtocol
1. Subclass See MockURLProtocol
URLSession
with your mock URLProtocol
2. Configure let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
//Use this URLSession instance to make requests.
let session = URLSession(configuration: config)
URLSession
instance just as you would.
4. Use the configured session.dataTask(with: urlRequest) { (data, response, error) in
//Stubbed response
}.resume()
UI Testing with Stubbed Network Data
The above method(as well as the famous OHHTTPStubs) doesn't work for UI testing because the test bundle and the app bundle (XCUIApplication) are loaded in separate processes. By using Swifter, you can run a local http server on the simulator.
First, change the API endpoints during UI testing with launchArguments in your hosting app.
//In XCTestCase,
override func setUp() {
app = XCUIApplication()
app.launchArguments = ["-uitesting"]
}
//In AppDelegate's application(_:didFinishLaunchingWithOptions:)
if ProcessInfo.processInfo.arguments.contains("-uitesting") {
BookStoreConfiguration.shared.setBaseURL(URL(string: "http://localhost:8080")!)
}
Then stub the network and test the UI with it.
let server = HttpServer()
func testNewBooksNormal() {
do {
let path = try TestUtil.path(for: normalResponseJSONFilename, in: type(of: self))
server[newBooksPath] = shareFile(path)
try server.start()
app.launch()
} catch {
XCTAssert(false, "Swifter Server failed to start.")
}
XCTContext.runActivity(named: "Test Successful TableView Screen") { _ in
XCTAssert(app.tables[tableViewIdentifier].waitForExistence(timeout: 3))
XCTAssert(app.tables[tableViewIdentifier].cells.count > 0)
XCTAssert(app.staticTexts["9781788476249"].exists)
XCTAssert(app.staticTexts["$44.99"].exists)
}
}
Using Frameworks for independent functionalities
Separating your app's functions into targets has several advantages. It forces you to care about dependencies, and it is good for unit tests since features are sandboxed. However, it may slow down the app launch (by little) due to framework loading.
BookStoreKit
is responsible for fetching and searching books data from IT Bookstore API.
Networking
is a wrapper around URLSession for making HTTP requests and parsing response.
Writing a documentation comment
Swift's API Design Guidelines suggest you write a documentation comment for every declaration. Writing one can have an impact on the design.
1. Write
Reference this document for markup formatting.
2. Check out the result
In Xcode's autocompletion
and Show Quick Help (option + click)
Getting Rid of IUOs
IMHO Implictly unwrapped optional is a potential threat to code safety and should be avoided as much as possible if not altogether. An example of two methods to get rid of them from where they are commonly used.
Make IBOutlets Optional
IBOutlets are IUOs by Apple's default. However, you can change that to Optional types. You may worry that making IBOutlets Optionals may cause too many if lets or guards, but that concern may just be overrated. IBOutlets are mostly used to set values on them, so optional chaining is sufficient. In just few cases where unwrapping is added, I will embrace them for additional safety of my code.
Using lazy instantiation
For the properties of UIViewController subclass, IUO can be useful but it's still dangerous. Instead, I use unspecified
. It generates a crash upon class/struct usage so it can be spotted fast during development, and most importantly no more IUOs.
//Inside a viewcontroller
lazy var bookStore: BookStoreService = unspecified()