Skip to content

Commit

Permalink
Doc, logging, other minor refinements in example project
Browse files Browse the repository at this point in the history
  • Loading branch information
pcantrell committed Nov 15, 2016
1 parent 6ed374e commit 4d24388
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 31 deletions.
11 changes: 6 additions & 5 deletions Examples/GithubBrowser/GithubBrowser.xcodeproj/project.pbxproj
Expand Up @@ -17,7 +17,7 @@
DA7462471B4C768B00406D67 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA7462451B4C768B00406D67 /* LaunchScreen.storyboard */; };
DAAC4E461D3AAD1B00FB3CE2 /* RepositoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAC4E451D3AAD1B00FB3CE2 /* RepositoryViewController.swift */; };
DAE27A801D3F1EF400D757F9 /* Optional+GithubBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE27A7F1D3F1EF400D757F9 /* Optional+GithubBrowser.swift */; };
DAE2F84A1B94F10500D2AD96 /* GithubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8441B94F10500D2AD96 /* GithubAPI.swift */; };
DAE2F84A1B94F10500D2AD96 /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8441B94F10500D2AD96 /* GitHubAPI.swift */; };
DAE2F84C1B94F10500D2AD96 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8471B94F10500D2AD96 /* AppDelegate.swift */; };
DAE2F84D1B94F10500D2AD96 /* RepositoryListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8481B94F10500D2AD96 /* RepositoryListViewController.swift */; };
DAE2F84E1B94F10500D2AD96 /* UserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8491B94F10500D2AD96 /* UserViewController.swift */; };
Expand All @@ -38,7 +38,7 @@
DA7462481B4C768B00406D67 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DAAC4E451D3AAD1B00FB3CE2 /* RepositoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryViewController.swift; sourceTree = "<group>"; };
DAE27A7F1D3F1EF400D757F9 /* Optional+GithubBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Optional+GithubBrowser.swift"; sourceTree = "<group>"; };
DAE2F8441B94F10500D2AD96 /* GithubAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubAPI.swift; sourceTree = "<group>"; };
DAE2F8441B94F10500D2AD96 /* GitHubAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = "<group>"; };
DAE2F8471B94F10500D2AD96 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
DAE2F8481B94F10500D2AD96 /* RepositoryListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryListViewController.swift; sourceTree = "<group>"; };
DAE2F8491B94F10500D2AD96 /* UserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -134,7 +134,7 @@
DAE2F8431B94F10500D2AD96 /* API */ = {
isa = PBXGroup;
children = (
DAE2F8441B94F10500D2AD96 /* GithubAPI.swift */,
DAE2F8441B94F10500D2AD96 /* GitHubAPI.swift */,
);
path = API;
sourceTree = "<group>";
Expand Down Expand Up @@ -282,7 +282,7 @@
DA2B556A1C7EDE7B00EB4D67 /* LoginViewController.swift in Sources */,
DA2B55621C7D6F8700EB4D67 /* User.swift in Sources */,
DAE2F84C1B94F10500D2AD96 /* AppDelegate.swift in Sources */,
DAE2F84A1B94F10500D2AD96 /* GithubAPI.swift in Sources */,
DAE2F84A1B94F10500D2AD96 /* GitHubAPI.swift in Sources */,
DAE27A801D3F1EF400D757F9 /* Optional+GithubBrowser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -350,6 +350,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_SWIFT_FLAGS = "-D DEBUG";
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
Expand Down Expand Up @@ -389,6 +390,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_SWIFT_FLAGS = "-D DEBUG";
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
Expand All @@ -402,7 +404,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = Source/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
"OTHER_SWIFT_FLAGS[arch=*]" = "-DDEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.GithubBrowser;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 3.0;
Expand Down
16 changes: 8 additions & 8 deletions Examples/GithubBrowser/README.md
@@ -1,8 +1,8 @@
# Siesta Example Project

This app allows you to type a Github username and see the user’s name, avatar, and repos.
This app allows you to search and view GitHub repositories.

This is a simple app, and intentionally minimizes things outside of Siesta’s purview: minimal models, minimal functionality, and bare bones UI. (Well, there is the gratuitous use of the Siesta color scheme!)
It is a simple app, and intentionally minimizes things outside of Siesta’s purview: minimal models, minimal functionality, and bare bones UI. (Well, there is the gratuitous use of the Siesta color scheme!)

## What’s interesting about it?

Expand All @@ -19,24 +19,24 @@ Siesta solves all these problems transparently, with minimal code.

## Files of note

- `Source/API/GithubAPI.swift` shows how to:
- `Source/API/GitHubAPI.swift` shows how to:

- set up a Siesta service,
- send an authentication header, and
- add a custom response transformers that:
- wrap all JSON responses with SwiftyJSON,
- map endpoints to models, and
- replace Siesta’s default error messages with Github-provided messages when present.
- replace Siesta’s default error messages with GitHub-provided messages when present.

- `Source/UI/UserViewController.swift` shows how to:

- use Siesta to propagate changes from a Resource to a UI,
- retarget a view controller at different Resources while it is visible,
- use `ResourceStatusOverlay` to show a spinner and default error message, and
- use Siesta’s caching, throttling, and delayed cancellation to manage a rapid series of requests triggered by keystrokes.

- `Source/UI/RepositoryListViewController.swift` shows how to:

- create a view controller which displays a Siesta resource determined by a parent VC and
- populate a table view with Siesta.

Expand All @@ -48,6 +48,6 @@ Siesta solves all these problems transparently, with minimal code.

## Rate limit errors?

If you hit the Github API’s rate limit while running the demo, press the “Log In” button. If you’re experimenting with the demo a lot, you can set `GITHUB_USER` and `GITHUB_PASS` environment variables in the “Run” build scheme to make the app automatically log you in on launch.
If you hit the GitHub API’s rate limit while running the demo, press the “Log In” button. If you’re experimenting with the demo a lot, you can set `GITHUB_USER` and `GITHUB_PASS` environment variables in the “Run” build scheme to make the app automatically log you in on launch.

You can use a [personal access token](https://github.com/settings/tokens) in place of your password. You don’t need to grant any permissions to your token for this app; just the public access will do.
65 changes: 49 additions & 16 deletions Examples/GithubBrowser/Source/API/GithubAPI.swift
Expand Up @@ -14,17 +14,22 @@ class _GithubAPI {

fileprivate init() {
#if DEBUG
LogCategory.enabled = [.network, .staleness]
#endif
// Bare-bones logging of which network calls Siesta makes:
LogCategory.enabled = [.network]

// Configuration
// For more info about how Siesta decides whether to make a network call,
// and when it broadcasts state updates to the app:
//LogCategory.enabled = LogCategory.common

service.configure("**") {
// The basicAuthHeader property’s didSet causes this config to be reapplied whenever auth changes.
// For the gory details of what Siesta’s up to:
//LogCategory.enabled = LogCategory.detailed
#endif

$0.headers["Authorization"] = self.basicAuthHeader
// Global configuration

// By default, Siesta parses JSON using Foundation JSONSerialization. This transformer wraps that with SwiftyJSON.
service.configure {
// By default, Siesta parses JSON using Foundation JSONSerialization.
// This transformer wraps that with SwiftyJSON.

$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])

Expand All @@ -34,14 +39,27 @@ class _GithubAPI {
$0.pipeline[.cleanup].add(GithubErrorMessageExtractor())
}

// Resource-specific configuration

service.configure("/search/**") {
$0.expirationTime = 10 // Refresh search results after 10 seconds (Siesta default is 30)
}

// Auth configuration

// Note the "**" pattern, which makes this config apply only to subpaths of baseURL.
// This prevents accidental credential leakage to untrusted servers.

service.configure("**") {
// This header configuration gets reapplied whenever the user logs in or out.
// How? See the basicAuthHeader property’s didSet.

$0.headers["Authorization"] = self.basicAuthHeader
}

// Mapping from specific paths to models

service.configureTransformer("/users/*") {
// Swift 3 TODO: see if bare $0 bug is finally fixed, or consider passing struct that still supports $0.content
try User(json: $0.content) // Input type inferred because User.init takes JSON
}

Expand All @@ -52,7 +70,8 @@ class _GithubAPI {
}

service.configureTransformer("/search/repositories") {
try ($0.content as JSON)["items"].arrayValue
try ($0.content as JSON)["items"]
.arrayValue
.map(Repository.init)
}

Expand All @@ -61,7 +80,7 @@ class _GithubAPI {
}

service.configure("/user/starred/*/*") { // Github gives 202 for “starred” and 404 for “not starred.”
$0.pipeline[.model].add( // This custom transformer turns that curious convention into
$0.pipeline[.model].add( // This custom transformer turns that curious convention into
TrueIfResourceFoundTransformer()) // a resource whose content is a simple boolean.
}

Expand Down Expand Up @@ -99,8 +118,8 @@ class _GithubAPI {
service.wipeResources() // Scrub all unauthenticated data

// Note that wipeResources() broadcasts a “no data” event to all observers of all resources.
// Therefore, if your UI diligently observes all the resources it uses, this call prevents sensitive data
// from lingering in the UI after logout.
// Therefore, if your UI diligently observes all the resources it displays, this call prevents sensitive
// data from lingering in the UI after logout.
}
}

Expand Down Expand Up @@ -134,7 +153,9 @@ class _GithubAPI {
}

func repository(_ repositoryModel: Repository) -> Resource {
return repository(ownedBy: repositoryModel.owner.login, named: repositoryModel.name)
return repository(
ownedBy: repositoryModel.owner.login,
named: repositoryModel.name)
}

func currentUserStarred(_ repositoryModel: Repository) -> Resource {
Expand All @@ -149,29 +170,41 @@ class _GithubAPI {
return starredResource
.request(isStarred ? .put : .delete)
.onSuccess { _ in
// Update succeeded. Directly update the locally cached “starred / not starred” state.

starredResource.overrideLocalContent(with: isStarred)
self.repository(repositoryModel).load() // To update star count

// Ask server for an updated star count. Note that we only need to trigger the load here, not handle
// the response! Any UI that is displaying the star count will be observing this resource, and thus
// will pick up the change. The code that knows _when_ to trigger the load is decoupled from the code
// that knows _what_ to do with the updated data. This is the magic of Siesta.

self.repository(repositoryModel).load()
}
}
}

/// Wraps all response entity content with SwiftyJSON
private let SwiftyJSONTransformer =
ResponseContentTransformer
ResponseContentTransformer(transformErrors: true)
{ JSON($0.content as AnyObject) }

/// If the response is JSON and has a "message" value, use it as the user-visible error message.
private struct GithubErrorMessageExtractor: ResponseTransformer {
func process(_ response: Response) -> Response {
switch response {
case .success:
return response

case .failure(var error):
error.userMessage = error.jsonDict["message"] as? String ?? error.userMessage
let json = error.typedContent(ifNone: JSON.null)
error.userMessage = json["message"].string ?? error.userMessage
return .failure(error)
}
}
}

/// Special handling for detecting whether repo is starred; see "/user/starred/*/*" config above
private struct TrueIfResourceFoundTransformer: ResponseTransformer {
func process(_ response: Response) -> Response {
switch response {
Expand Down
3 changes: 1 addition & 2 deletions Source/Siesta/Support/Logging.swift
Expand Up @@ -27,8 +27,7 @@ public enum LogCategory
/// `ResourceEvent` broadcast by resources.
case stateChanges

/// Detailed information about which events are sent to which observers, when they are added, and when they are
/// removed.
/// Detailed information about when observers are added, when they are removed, and which events they receive.
case observers

/// Information about how `Resource.loadIfNeeded()` decides whether to initiate a request.
Expand Down

0 comments on commit 4d24388

Please sign in to comment.