{"id":13694290,"url":"https://github.com/nsoojin/BookStore-iOS","last_synced_at":"2025-05-03T01:32:38.211Z","repository":{"id":46253226,"uuid":"201909329","full_name":"nsoojin/BookStore-iOS","owner":"nsoojin","description":" Sample iOS App  - A collection of examples and patterns for Unit Testing, UI Testing, handling Result/Optionals, writing documentation, and more. Details in README.","archived":false,"fork":false,"pushed_at":"2021-11-03T19:51:21.000Z","size":14276,"stargazers_count":234,"open_issues_count":2,"forks_count":41,"subscribers_count":11,"default_branch":"master","last_synced_at":"2024-11-07T08:01:56.768Z","etag":null,"topics":["ios","ios-app","ios-application-testing","ios-code-coverage","ios-demo","ios-design-patterns","ios-develop-tips","ios-learning","ios-sample","ios-swift","open-api","stub-networking","swift","swift-documentation","swift-framework","ui-testing","unit-testing","xctest","xcuitest"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nsoojin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":"soojinro","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2019-08-12T10:32:49.000Z","updated_at":"2024-09-14T14:46:05.000Z","dependencies_parsed_at":"2022-08-27T08:00:19.039Z","dependency_job_id":null,"html_url":"https://github.com/nsoojin/BookStore-iOS","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsoojin%2FBookStore-iOS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsoojin%2FBookStore-iOS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsoojin%2FBookStore-iOS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsoojin%2FBookStore-iOS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nsoojin","download_url":"https://codeload.github.com/nsoojin/BookStore-iOS/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224346493,"owners_count":17296223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","ios-app","ios-application-testing","ios-code-coverage","ios-demo","ios-design-patterns","ios-develop-tips","ios-learning","ios-sample","ios-swift","open-api","stub-networking","swift","swift-documentation","swift-framework","ui-testing","unit-testing","xctest","xcuitest"],"created_at":"2024-08-02T17:01:28.771Z","updated_at":"2024-11-12T20:32:05.828Z","avatar_url":"https://github.com/nsoojin.png","language":"Swift","readme":"# BookStore\n\n\u003cp align=\"left\"\u003e\n    \u003ca href=\"https://travis-ci.org/nsoojin/BookStore-iOS\" alt=\"Build\"\u003e\n        \u003cimg src=\"https://travis-ci.org/nsoojin/BookStore-iOS.svg?branch=master\"/\u003e\u003c/a\u003e\n    \u003ca href=\"https://codecov.io/gh/nsoojin/BookStore-iOS\"\u003e\n      \u003cimg src=\"https://codecov.io/gh/nsoojin/BookStore-iOS/branch/master/graph/badge.svg\" /\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/nsoojin/BookStore-iOS/pulls\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/PRs-welcome-informational\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/nsoojin/BookStore/blob/master/LICENSE\" alt=\"License\"\u003e\n        \u003cimg src=\"https://img.shields.io/github/license/nsoojin/BookStore-iOS\"/\u003e\u003c/a\u003e\n    \u003ca href=\"https://twitter.com/intent/follow?screen_name=soojinro\"\u003e\n        \u003cimg src=\"https://img.shields.io/twitter/follow/soojinro?style=social\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n### 👉 [한글 버전](https://soojin.ro/blog/bookstore-ios-readme)\n\nSee new releases and search for programming books from [IT Bookstore API](https://api.itbook.store)\n\nThis is a sample app to practice using `Result` type, stubbing network request for unit tests, separating functionalities into frameworks, and writing Swift documentation.\n\n### How to run\n\n```\n\u003e cd BookStore\n\u003e open BookStore.xcodeproj\n```\n\nRun!\n\n# Contents\n\n- [App Features](https://github.com/nsoojin/BookStore-iOS#app-features)\n\n- [`Result` type in Swift 5](https://github.com/nsoojin/BookStore-iOS#result-type-in-swift-5)\n\n- [Stubbing Network Requests for Unit Tests](https://github.com/nsoojin/BookStore-iOS#stubbing-network-requests-for-unit-tests)\n\n- [UI Testing with Stubbed Network Data](https://github.com/nsoojin/BookStore-iOS#ui-testing-with-stubbed-network-data)\n\n- [Using Frameworks for independent functionalities](https://github.com/nsoojin/BookStore-iOS#using-frameworks-for-independent-functionalities)\n\n- [Writing a documentation comment](https://github.com/nsoojin/BookStore-iOS#writing-a-documentation-comment)\n\n- [Getting Rid of IUOs](https://github.com/nsoojin/BookStore-iOS#getting-rid-of-iuos)\n\n## App Features\n\n### What's New\n\nA simple `UITableView` with cells and modal presentation for a detailed page.\n\n\u003cimg src=\"https://raw.githubusercontent.com/nsoojin/BookStore/master/README_assets/whats-new.gif\" width=\"400\"\u003e\n\n### Search\n\n1. 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](https://github.com/nsoojin/BookStore/blob/3a3e91f903e5a77ebdcafd53803fd3edad0dde65/BookStoreKit/Utils/Debouncer.swift#L11).\n\n2. Search results are paginated and provides infinite scroll.\n\n\u003cimg src=\"https://raw.githubusercontent.com/nsoojin/BookStore/master/README_assets/search.gif\" width=\"400\"\u003e\n\n## `Result` type in Swift 5\n\nOut of the box, you have to switch on the `Result` instance to access the underlying success instance or the error instance. \n\n```Swift\nswitch result {\ncase .success(let response):\n  //do something with the response\ncase .failure(let error):\n  //handle error\n}\n```\n\nHowever, I think switch statements are too wordy. I added [`success`](https://github.com/nsoojin/BookStore/blob/5f3d7d85503fd1de51d04b0286653a3578a7125a/BookStore/Extensions/Result%20%2B%20Extensions.swift#L25) and [`catch`](https://github.com/nsoojin/BookStore/blob/5f3d7d85503fd1de51d04b0286653a3578a7125a/BookStore/Extensions/Result%20%2B%20Extensions.swift#L34) method to `Result` type. So it can be chained like this.\n\n```Swift\nsearchResult.success { response in\n  //do something with the response\n}.catch { error in\n  //handle error\n}\n```\n\nEven cleaner, like this.\n\n```Swift\nresult.success(handleSuccess)\n      .catch(handleError)\n      \nfunc handleSuccess(_ result: SearchResult) { ... }\nfunc handleError(_ error: Error) { ... }\n```\n\n## Stubbing Network Requests for Unit Tests\n\nGenerally, 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`.\n\n### 1. Subclass `URLProtocol`\n\nSee [`MockURLProtocol`](https://github.com/nsoojin/BookStore/blob/master/BookStoreKitTests/MockAPI/MockURLProtocol.swift)\n\n### 2. Configure `URLSession` with your mock `URLProtocol`\n\n```Swift\nlet config = URLSessionConfiguration.ephemeral\nconfig.protocolClasses = [MockURLProtocol.self]\n\n//Use this URLSession instance to make requests.\nlet session = URLSession(configuration: config) \n```\n\n### 4. Use the configured `URLSession` instance just as you would.\n\n```Swift\nsession.dataTask(with: urlRequest) { (data, response, error) in\n  //Stubbed response\n}.resume()\n```\n\n## UI Testing with Stubbed Network Data\n\nThe above method(as well as the famous [OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)) doesn't work for UI testing because the test bundle and the app bundle (XCUIApplication) are loaded in separate processes. By using [Swifter](https://github.com/httpswift/swifter), you can run a local http server on the simulator. \n\nFirst, change the API endpoints during UI testing with launchArguments in your hosting app.\n\n```swift\n//In XCTestCase,\noverride func setUp() {\n  app = XCUIApplication()\n  app.launchArguments = [\"-uitesting\"]\n}\n\n//In AppDelegate's application(_:didFinishLaunchingWithOptions:)\nif ProcessInfo.processInfo.arguments.contains(\"-uitesting\") {\n  BookStoreConfiguration.shared.setBaseURL(URL(string: \"http://localhost:8080\")!)\n}\n```\n\nThen stub the network and test the UI with it.\n\n```swift\nlet server = HttpServer()\n\nfunc testNewBooksNormal() {\n  do {\n    let path = try TestUtil.path(for: normalResponseJSONFilename, in: type(of: self))\n    server[newBooksPath] = shareFile(path)\n    try server.start()\n    app.launch()\n  } catch {\n    XCTAssert(false, \"Swifter Server failed to start.\")\n  }\n        \n  XCTContext.runActivity(named: \"Test Successful TableView Screen\") { _ in\n    XCTAssert(app.tables[tableViewIdentifier].waitForExistence(timeout: 3))\n    XCTAssert(app.tables[tableViewIdentifier].cells.count \u003e 0)\n    XCTAssert(app.staticTexts[\"9781788476249\"].exists)\n    XCTAssert(app.staticTexts[\"$44.99\"].exists)\n  }\n}\n```\n\n## Using Frameworks for independent functionalities\n\nSeparating 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.\n\n`BookStoreKit` is responsible for fetching and searching books data from [IT Bookstore API](https://api.itbook.store). \n\n`Networking` is a wrapper around URLSession for making HTTP requests and parsing response.\n\n\u003cimg src=\"https://raw.githubusercontent.com/nsoojin/BookStore/master/README_assets/xcodeproj-targets.png\" width=\"200\"\u003e\n\n## Writing a documentation comment\n\n[Swift's API Design Guidelines](https://swift.org/documentation/api-design-guidelines/#fundamentals) suggest you write a documentation comment for every declaration. Writing one can have an impact on the design.\n\n### 1. Write\n\nReference this [document](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/index.html) for markup formatting.\n\n\u003cimg src=\"https://raw.githubusercontent.com/nsoojin/BookStore/master/README_assets/documentation-0.png\"\u003e\n\n### 2. Check out the result\n\nIn Xcode's autocompletion\n\n\u003cimg src=\"https://raw.githubusercontent.com/nsoojin/BookStore/master/README_assets/documentation-1.png\" width=\"400\"\u003e\n\nand Show Quick Help (option + click)\n\n\u003cimg src=\"https://raw.githubusercontent.com/nsoojin/BookStore/master/README_assets/documentation-2.png\" width=\"600\"\u003e\n\n## Getting Rid of IUOs\n\nIMHO 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.\n\n### Make IBOutlets Optional\n\nIBOutlets 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.\n\n\u003cimg src=\"https://raw.githubusercontent.com/nsoojin/BookStore/master/README_assets/optional-iboutlets.png\" width=\"500\"\u003e\n\n### Using lazy instantiation\n\nFor the properties of UIViewController subclass, IUO can be useful but it's still dangerous. Instead, I use [`unspecified`](https://github.com/nsoojin/BookStore/blob/e27ea7252189e9f7ed2b7a9494334ccab9ce801c/BookStore/Extensions/Unspecified.swift#L11). It generates a crash upon class/struct usage so it can be spotted fast during development, and most importantly no more IUOs.\n\n```swift\n//Inside a viewcontroller\nlazy var bookStore: BookStoreService = unspecified()\n```\n","funding_links":["https://ko-fi.com/soojinro"],"categories":["Misc"],"sub_categories":["Notes"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnsoojin%2FBookStore-iOS","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnsoojin%2FBookStore-iOS","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnsoojin%2FBookStore-iOS/lists"}