Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ xcuserdata
Carthage/
.build
Siesta.xcodeproj/xcshareddata/xcbaselines
.idea/
1 change: 1 addition & 0 deletions Cartfile.private
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

github "Alamofire/Alamofire"
# github "ReactiveCocoa/ReactiveCocoa" "master" # add to Cartfile if/when it has tests
github "ReactiveX/RxSwift"

# Testing

Expand Down
1 change: 1 addition & 0 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
github "Alamofire/Alamofire" "5.0.5"
github "Quick/Nimble" "v8.0.5"
github "ReactiveX/RxSwift" "5.1.1"
github "pcantrell/Quick" "d91676d00600c42f9b55349765b1f1099141bfba"
371 changes: 366 additions & 5 deletions Examples/GithubBrowser/GithubBrowser.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions Examples/GithubBrowser/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,20 @@ target 'GithubBrowser' do

pod 'Siesta/UI', path: '../..'
end

target "GithubBrowserCombine" do
use_frameworks!
platform :ios, '13.0'
pod 'Siesta/UI', path: '../..'
pod 'CombineCocoa'
pod 'CombineExt'
pod 'CombineDataSources'
end

target "GithubBrowserRx" do
use_frameworks!
pod 'Siesta/UI', path: '../..'
pod 'RxSwift'
pod 'RxCocoa'
pod 'RxOptional'
end
37 changes: 36 additions & 1 deletion Examples/GithubBrowser/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
PODS:
- CombineCocoa (0.2.0)
- CombineDataSources (0.2.5)
- CombineExt (1.2.0)
- RxCocoa (5.1.1):
- RxRelay (~> 5)
- RxSwift (~> 5)
- RxOptional (4.1.0):
- RxCocoa (~> 5)
- RxSwift (~> 5)
- RxRelay (5.1.1):
- RxSwift (~> 5)
- RxSwift (5.1.1)
- Siesta/Core (1.5.2)
- Siesta/UI (1.5.2):
- Siesta/Core

DEPENDENCIES:
- CombineCocoa
- CombineDataSources
- CombineExt
- RxCocoa
- RxOptional
- RxSwift
- Siesta/UI (from `../..`)

SPEC REPOS:
trunk:
- CombineCocoa
- CombineDataSources
- CombineExt
- RxCocoa
- RxOptional
- RxRelay
- RxSwift

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

SPEC CHECKSUMS:
CombineCocoa: 92b2579a9e4a364f8ba94be7fc68335bd3d73d92
CombineDataSources: dcbc3b06a038ffdf620b3b4533d3ecaf76ca6a2e
CombineExt: f6ad3c8e9a0c85f06e5c66ddd85eb32875d906d2
RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601
RxOptional: b1fcd60856807a564c0215c2184b8d33e7826dc2
RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9
RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178
Siesta: d1e1966af43ffca170f658ad6d987228a5b40873

PODFILE CHECKSUM: 974001388daa9ecbfa915ea0bc4093a33242099c
PODFILE CHECKSUM: 71f3353904f6db031a60fe5063ce84284832882f

COCOAPODS: 1.10.0
6 changes: 6 additions & 0 deletions Examples/GithubBrowser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@ Siesta solves all these problems transparently, with minimal code.
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.

## Combine and RxSwift

The main view controllers have *Combine and *Rx variants to demonstrate the use of Siesta's reactive extensions, and the project has accompanying Combine and Rx targets.

You might find it instructive to compare the reactive controllers with their non-reactive versions, particularly if you're an existing Siesta user thinking of adopting the reactive extensions.
15 changes: 15 additions & 0 deletions Examples/GithubBrowser/Source/API/GitHubAPI+Combine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Combine

extension _GitHubAPI {

/*
An unintrusive way to publish isAuthenticated - _GitHubAPI is shared with the other non-Combine examples.
In reality if we were using a stored property we'd annotate with @Published.
*/
var isAuthenticatedPublisher: AnyPublisher<Bool, Never> {
publisher(for: \.basicAuthHeader)
.map { $0 != nil }
.eraseToAnyPublisher()
}

}
14 changes: 14 additions & 0 deletions Examples/GithubBrowser/Source/API/GitHubAPI+Rx.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import RxSwift
import RxCocoa

extension _GitHubAPI {

/*
An unintrusive way to publish isAuthenticated - _GitHubAPI is shared with the other non-rx examples.
In reality we'd do this differently.
*/
var isAuthenticatedObservable: Observable<Bool> {
rx.observe(String?.self, "basicAuthHeader").map { $0.map { $0 != nil} ?? false }.distinctUntilChanged()
}

}
19 changes: 16 additions & 3 deletions Examples/GithubBrowser/Source/API/GithubAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import Siesta

let GitHubAPI = _GitHubAPI()

class _GitHubAPI {
class _GitHubAPI: NSObject { /* NSObject just for reactive extensions */

// MARK: - Configuration

private let service = Service(
baseURL: "https://api.github.com",
standardTransformers: [.text, .image]) // No .json because we use Swift 4 JSONDecoder instead of older JSONSerialization

fileprivate init() {
fileprivate override init() {
super.init()

#if DEBUG
// Bare-bones logging of which network calls Siesta makes:
SiestaLog.Category.enabled = [.network]
Expand Down Expand Up @@ -128,7 +130,8 @@ class _GitHubAPI {
return basicAuthHeader != nil
}

private var basicAuthHeader: String? {
/* "@objc dynamic" just for reactive extensions */
@objc dynamic var basicAuthHeader: String? {
didSet {
// These two calls are almost always necessary when you have changing auth for your API:

Expand Down Expand Up @@ -181,6 +184,16 @@ class _GitHubAPI {
named: repositoryModel.name)
}

func contributors(_ repositoryModel: Repository) -> Resource? {
return repository(repositoryModel)
.optionalRelative(repositoryModel.contributorsURL)
}

func languages(_ repositoryModel: Repository) -> Resource? {
return repository(repositoryModel)
.optionalRelative(repositoryModel.languagesURL)
}

func currentUserStarred(_ repositoryModel: Repository) -> Resource {
return service
.resource("/user/starred")
Expand Down
40 changes: 40 additions & 0 deletions Examples/GithubBrowser/Source/Info-Combine.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
40 changes: 40 additions & 0 deletions Examples/GithubBrowser/Source/Info-Rx.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import UIKit
import Siesta
import Combine
import CombineExt
import CombineDataSources

class RepositoryListViewController: UITableViewController {

private var statusOverlay = ResourceStatusOverlay()
private var subs = [AnyCancellable]()

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = SiestaTheme.darkColor
statusOverlay.embed(in: self)

// Standard table view stuff we don't want here (we're sharing the storyboard with the other examples)
tableView.dataSource = nil
tableView.delegate = nil
}

override func viewDidLayoutSubviews() {
statusOverlay.positionToCoverParent()
}

/**
The input to this class - the repositories to display.

Whether it's better to pass in a resource or a publisher here is much the same argument as whether to define
APIs in terms of resources or publishers. See UserViewController for a discussion about that.
*/
func configure(repositories: AnyPublisher<Resource? /* [Repository] */, Never>) {
/*
Oh hey, in the next small handful of lines, let's:
- make an api call if necessary to fetch the latest repo list we're to show
- display progress and errors while doing that, and
- populate the table.
*/
repositories
// In this project we have an extension to tell Siesta's status overlay to be
// interested in the latest Resource output by a Resource publisher. The following
// line gives us a progress spinner, error display and retry functionality.
.watchedBy(statusOverlay: statusOverlay)

// Transform the sequence of Resources into a sequence of their content: [Repository].
.flatMapLatest { resource -> AnyPublisher<[Repository], Never> in
resource?.contentPublisher() ?? Just([]).eraseToAnyPublisher()
}

// This is everything we need to populate the table with the list of repos,
// courtesy of CombineDataSources.
.bind(subscriber: tableView.rowsSubscriber(cellIdentifier: "repo", cellType: RepositoryTableViewCell.self, cellConfig: { cell, indexPath, repo in
cell.repository = repo
}))
.store(in: &subs)
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "repoDetail" {
if let repositoryVC = segue.destination as? RepositoryViewController,
let cell = sender as? RepositoryTableViewCell {

if let repo = cell.repository {
repositoryVC.repositoryResource = Just(GitHubAPI.repository(repo)).eraseToAnyPublisher()
}
}
}
}
}

class RepositoryTableViewCell: UITableViewCell {
@IBOutlet weak var icon: RemoteImageView!
@IBOutlet weak var userLabel: UILabel!
@IBOutlet weak var repoLabel: UILabel!
@IBOutlet weak var starCountLabel: UILabel!

var repository: Repository? {
didSet {
userLabel.text = repository?.owner.login
repoLabel.text = repository?.name
starCountLabel.text = repository?.starCount?.description

// Note how powerful this next line is:
//
// • RemoteImageView calls loadIfNeeded() when we set imageURL, so this automatically triggers a network
// request for the image. However...
//
// • loadIfNeeded() won’t make redundant requests, so no need to worry about whether this avatar is used in
// other table cells, or whether we’ve already requested it! Many cells sharing one image spawn one
// request. One response updates _all_ the cells that image.
//
// • If imageURL was already set, RemoteImageView calls cancelLoadIfUnobserved() on the old image resource.
// This means that if the user is scrolling fast and table cells are being reused:
//
// - a request in progress gets cancelled
// - unless other cells are also waiting on the same image, in which case the request continues, and
// - an image that we’ve already fetch stay available in memory, fully parsed & ready for instant resuse.
//
// Finally, note that all of this nice behavior is not special magic that’s specific to images. These are
// basic Siesta behaviors you can use for resources of any kind. Look at the RemoteImageView source code
// and study how it uses the core Siesta API.

icon.imageURL = repository?.owner.avatarURL
}
}
}

// Required by CombineDataSources for binding the repositories to the table
extension Repository: Hashable {
public func hash(into hasher: inout Hasher) { url.hash(into: &hasher) }

public static func ==(lhs: Repository, rhs: Repository) -> Bool { lhs.url == rhs.url }
}
Loading