Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] @ResourceBacked property wrapper #307

Open
wants to merge 28 commits into
base: file-cache
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7ad7833
EntityCache now allows specific ContentType instead of Any
pcantrell Mar 9, 2018
65be5bd
Corrections to the EntityCache API docs
pcantrell Nov 27, 2017
c7f45cd
New SiestaTools subproject
pcantrell Jul 16, 2019
d49bca2
Initial FileCache implementation
pcantrell Jul 17, 2019
4a97c9c
Clarified log message
pcantrell Jun 18, 2018
5f9233c
EntityCache methods can now throw
pcantrell Mar 13, 2018
3e13b83
FileCache data isolation
pcantrell Mar 17, 2018
3e53455
Including FileCache.ContentType in keys
pcantrell Mar 18, 2018
a859c66
More legible cache logging
pcantrell Jun 18, 2018
e95c7c8
Entity can be codable in Swift 5
pcantrell Mar 31, 2019
470b67a
Swift 5 fixes
pcantrell Mar 31, 2019
4c8c6de
Removed deprecated isCompleted from comments/spec names
pcantrell Apr 18, 2019
c177347
Lint warning for disabled specs
pcantrell Apr 22, 2019
ae7675a
completeFileProtection is unavailable on macOS
pcantrell Mar 23, 2020
78e474c
Mopping up cache API changes
pcantrell Apr 11, 2019
38ef19c
Experimenting with FileCache in GHBrowser
pcantrell Mar 28, 2019
562822f
Merge 1.5 changes into file-cache
pcantrell Apr 3, 2020
8a6c486
Tuck cache actions inside ResponseInfo, let Resource control whether …
pcantrell Apr 4, 2020
3987fb9
Caching finally limited to GETs on same resource
pcantrell Apr 4, 2020
af0ce95
Made cache requests behave correctly if passed to load(using:)
pcantrell Apr 4, 2020
6b825f8
Fixed minor leak in ObjC specs
pcantrell Apr 4, 2020
8b1dd28
Fixed cache timestamp handling
pcantrell Apr 5, 2020
3b5b834
Rerunning a cache request needs to clear earlier cache stages
pcantrell Apr 5, 2020
d8b4321
Show stale cached data during load in example project
pcantrell Apr 6, 2020
03ba271
Tidied up cache-related logging
pcantrell Apr 6, 2020
47c400b
Oops, where did example project's scheme go?
pcantrell Apr 8, 2020
01bd85a
ResourceBacked property wrapper first crack
pcantrell Apr 8, 2020
a42150c
Trying property wrapper in example project
pcantrell Apr 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cartfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA7462381B4C768B00406D67"
BuildableName = "GithubBrowser.app"
BlueprintName = "GithubBrowser"
ReferencedContainer = "container:GithubBrowser.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA7462381B4C768B00406D67"
BuildableName = "GithubBrowser.app"
BlueprintName = "GithubBrowser"
ReferencedContainer = "container:GithubBrowser.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DA7462381B4C768B00406D67"
BuildableName = "GithubBrowser.app"
BlueprintName = "GithubBrowser"
ReferencedContainer = "container:GithubBrowser.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
1 change: 1 addition & 0 deletions Examples/GithubBrowser/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ target 'GithubBrowser' do
use_frameworks!

pod 'Siesta/UI', path: '../..'
pod 'Siesta/Tools', path: '../..'
end
7 changes: 5 additions & 2 deletions Examples/GithubBrowser/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
PODS:
- Siesta/Core (1.5.0)
- Siesta/Tools (1.5.0):
- Siesta/Core
- Siesta/UI (1.5.0):
- Siesta/Core

DEPENDENCIES:
- Siesta/Tools (from `../..`)
- Siesta/UI (from `../..`)

EXTERNAL SOURCES:
Siesta:
:path: "../.."

SPEC CHECKSUMS:
Siesta: 407b99ae05344d2de33d98e9e239551086daee6a
Siesta: 31a12f6f9905bd144cfadaa861a0cdd55c2faa17

PODFILE CHECKSUM: 974001388daa9ecbfa915ea0bc4093a33242099c
PODFILE CHECKSUM: 96de1def0845d136c0ee748e2599bcfc49e6b814

COCOAPODS: 1.9.1
43 changes: 41 additions & 2 deletions Examples/GithubBrowser/Source/API/GithubAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class _GitHubAPI {
fileprivate init() {
#if DEBUG
// Bare-bones logging of which network calls Siesta makes:
SiestaLog.Category.enabled = [.network]
SiestaLog.Category.enabled = [.network, .cache]

// For more info about how Siesta decides whether to make a network call,
// and which state updates it broadcasts to the app:
Expand All @@ -43,13 +43,48 @@ class _GitHubAPI {

$0.pipeline[.cleanup].add(
GitHubErrorMessageExtractor(jsonDecoder: jsonDecoder))

// Cache API results for fast launch & offline access:

$0.pipeline[.rawData].cacheUsing {
try FileCache<Data>(
poolName: "api.github.com",
dataIsolation: .perUser(identifiedBy: self.username)) // Show each user their own data
}

// Using the closure form of cacheUsing above signals that if we encounter an error trying create a cache
// directory or generate a cache isolation key from the username, we should simply proceed silently without
// having a persistent cache.

// Note that the dataIsolation uses only username. This means that users will not _see_ other users’ data;
// however, it does not _secure_ one user’s data from another. A user with permission to see the cache
// directory could in principle see all the cached data.
//
// To fully secure one user’s data from another, the application would need to generate some long-lived
// secret that is unique to each user. A password can work, though it will essentially empty the user’s
// cache if the password changes. The server could also send some kind of high-entropy per-user token in
// the authentication response.
}

RemoteImageView.defaultImageService.configure {
// We can cache images offline too:

$0.pipeline[.rawData].cacheUsing {
try FileCache<Data>(
poolName: "images",
dataIsolation: .sharedByAllUsers) // images aren't secret, so no need to isolate them
}
}


// –––––– Resource-specific configuration ––––––

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

// Don't cache search results between runs, so we don't see stale results on launch
$0.pipeline.removeAllCaches()
}

// –––––– Auth configuration ––––––
Expand Down Expand Up @@ -115,19 +150,23 @@ class _GitHubAPI {
// MARK: - Authentication

func logIn(username: String, password: String) {
if let auth = "\(username):\(password)".data(using: String.Encoding.utf8) {
self.username = username
if let auth = "\(username):\(password)".data(using: .utf8) {
basicAuthHeader = "Basic \(auth.base64EncodedString())"
}
}

func logOut() {
username = nil
basicAuthHeader = nil
}

var isAuthenticated: Bool {
return basicAuthHeader != nil
}

private var username: String?

private var basicAuthHeader: String? {
didSet {
// These two calls are almost always necessary when you have changing auth for your API:
Expand Down
32 changes: 10 additions & 22 deletions Examples/GithubBrowser/Source/UI/RepositoryListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,13 @@ class RepositoryListViewController: UITableViewController, ResourceObserver {

// MARK: Interesting Siesta stuff

var repositoriesResource: Resource? {
didSet {
oldValue?.removeObservers(ownedBy: self)

repositoriesResource?
.addObserver(self)
.addObserver(statusOverlay, owner: self)
.loadIfNeeded()
}
}

var repositories: [Repository] = [] {
didSet {
tableView.reloadData()
}
}
@ResourceBacked(default: [])
var repositories: [Repository]

var statusOverlay = ResourceStatusOverlay()

func resourceChanged(_ resource: Resource, event: ResourceEvent) {
// Siesta’s typedContent() infers from the type of the repositories property that
// repositoriesResource should hold content of type [Repository].

repositories = repositoriesResource?.typedContent() ?? []
tableView.reloadData()
}

// MARK: Standard table view stuff
Expand All @@ -37,7 +20,12 @@ class RepositoryListViewController: UITableViewController, ResourceObserver {
super.viewDidLoad()

view.backgroundColor = SiestaTheme.darkColor

statusOverlay.embed(in: self)
statusOverlay.displayPriority = [.anyData, .loading, .error]

$repositories.addObserver(self)
$repositories.addObserver(statusOverlay)
}

override func viewDidLayoutSubviews() {
Expand Down Expand Up @@ -65,8 +53,8 @@ class RepositoryListViewController: UITableViewController, ResourceObserver {
if let repositoryVC = segue.destination as? RepositoryViewController,
let cell = sender as? RepositoryTableViewCell {

repositoryVC.repositoryResource =
repositoriesResource?.optionalRelative(
repositoryVC.$repository.resource =
$repositories.resource?.optionalRelative(
cell.repository?.url)
}
}
Expand Down
75 changes: 23 additions & 52 deletions Examples/GithubBrowser/Source/UI/RepositoryViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,48 +25,25 @@ class RepositoryViewController: UIViewController, ResourceObserver {

// MARK: Resources

var repositoryResource: Resource? {
didSet {
updateObservation(from: oldValue, to: repositoryResource)
}
}
@ResourceBacked(default: nil)
var repository: Repository?

var starredResource: Resource? {
didSet {
updateObservation(from: oldValue, to: starredResource)
}
}
@ResourceBacked(default: false)
var isStarred: Bool

var languagesResource: Resource? {
didSet {
updateObservation(from: oldValue, to: languagesResource)
}
}
@ResourceBacked(default: [:])
var languages: [String:Int]

var contributorsResource: Resource? {
didSet {
updateObservation(from: oldValue, to: contributorsResource)
}
}
@ResourceBacked(default: [])
var contributors: [User]

private func updateObservation(from oldResource: Resource?, to newResource: Resource?) {
guard oldResource != newResource else { return }

oldResource?.removeObservers(ownedBy: self)
newResource?
.addObserver(self)
.addObserver(statusOverlay, owner: self)
.loadIfNeeded()
}

// MARK: Content conveniences

var repository: Repository? {
return repositoryResource?.typedContent()
}

var isStarred: Bool {
return starredResource?.typedContent() ?? false
for resourceBox in [$repository, $isStarred, $languages, $contributors] as [AnyResourceBacked] {
resourceBox.addObserver(self)
resourceBox.addObserver(statusOverlay)
}
}

// MARK: Display
Expand All @@ -75,6 +52,7 @@ class RepositoryViewController: UIViewController, ResourceObserver {
super.viewDidLoad()

view.backgroundColor = SiestaTheme.darkColor

statusOverlay.embed(in: self)
statusOverlay.displayPriority = [.anyData, .loading, .error] // Prioritize partial data over loading indicator

Expand Down Expand Up @@ -117,37 +95,30 @@ class RepositoryViewController: UIViewController, ResourceObserver {
descriptionLabel?.text = repository?.description
homepageButton?.setTitle(repository?.homepage, for: .normal)

if let contributors: [User] = contributorsResource?.typedContent() {
contributorsLabel?.text = contributors
.map { $0.login }
.joined(separator: "\n")
} else {
contributorsLabel?.text = "–"
}
contributorsLabel?.text = contributors
.map { $0.login }
.joined(separator: "\n")

if let languages: [String:Int] = languagesResource?.typedContent() {
languagesLabel?.text = languages.keys.joined(separator: " • ")
} else {
languagesLabel?.text = "–"
}
languagesLabel?.text = languages.keys.joined(separator: " • ")
}

func showStarred() {
if let repository = repository {
starredResource = GitHubAPI.currentUserStarred(repository)
$isStarred.resource = GitHubAPI.currentUserStarred(repository)
} else {
starredResource = nil
$isStarred.resource = nil
}

contributorsResource = repositoryResource?.optionalRelative(
$contributors.resource = $repository.resource?.optionalRelative(
repository?.contributorsURL)
languagesResource = repositoryResource?.optionalRelative(
$languages.resource = $repository.resource?.optionalRelative(
repository?.languagesURL)

starCountLabel?.text = repository?.starCount?.description
starIcon?.text = isStarred ? "★" : "☆"
starButton?.setTitle(isStarred ? "Unstar" : "Star", for: .normal)
starButton?.isEnabled = (repository != nil)
starButton?.isEnabled = ($isStarred.resource != nil)
starButton?.alpha = ($isStarred.resource != nil) ? 1 : 0.3
}

// MARK: Actions
Expand Down
4 changes: 3 additions & 1 deletion Examples/GithubBrowser/Source/UI/UserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class UserViewController: UIViewController, UISearchBarDelegate, ResourceObserve
view.backgroundColor = SiestaTheme.darkColor

statusOverlay.embed(in: self)
statusOverlay.displayPriority = [.anyData, .loading, .error]

showUser(nil)

searchBar.becomeFirstResponder()
Expand Down Expand Up @@ -144,7 +146,7 @@ class UserViewController: UIViewController, UISearchBarDelegate, ResourceObserve

// Setting the repositoriesResource property of the embedded VC triggers load & display of the user’s repos.

repoListVC?.repositoriesResource = repositoriesResource
repoListVC?.$repositories.resource = repositoriesResource
usernameLabel.text = title
}

Expand Down
6 changes: 6 additions & 0 deletions Siesta.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ Pod::Spec.new do |s|
s.exclude_files = "**/Info*.plist"
end

s.subspec "Tools" do |s|
s.source_files = "Source/SiestaTools/**/*"
s.exclude_files = "**/Info*.plist"
s.dependency "Siesta/Core"
end

s.subspec "UI" do |s|
s.ios.source_files = "Source/SiestaUI/**/*.{swift,m,h}"
s.dependency "Siesta/Core"
Expand Down
Loading