Created
November 19, 2021 23:00
-
-
Save danielt1263/42080cc1f5eebb37f8eb904d2c787358 to your computer and use it in GitHub Desktop.
This gist contains a couple of examples of using the new cycle functionality using my CLE architecture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Cause_Logic_Effect | |
import RxCocoa | |
import RxSwift | |
import RxSwiftExt | |
import UIKit | |
final class ViewController: UIViewController { | |
var tableView: UITableView! | |
var logoutButton: UIButton! | |
let disposeBag = DisposeBag() | |
override func loadView() { | |
super.loadView() | |
logoutButton = UIButton(type: .system).setup { | |
$0.setTitle("Logout", for: .normal) | |
$0.sizeToFit() | |
} | |
var headerFrame = view.bounds | |
headerFrame.size.height = 44 | |
let headerView = UIView(frame: headerFrame).setup { | |
$0.addSubview(logoutButton) | |
} | |
tableView = UITableView(frame: view.bounds).setup { | |
$0.autoresizingMask = [.flexibleWidth, .flexibleHeight] | |
$0.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") | |
$0.tableHeaderView = headerView | |
} | |
view.addSubview(tableView) | |
} | |
} | |
extension ViewController { | |
func connect() { | |
let api = API() | |
let token = cycle( | |
input: logoutButton.rx.tap.map(to: TokenLogic.Input.logout), | |
initialState: "", | |
reduce: TokenLogic.reducer, | |
reaction: reaction( | |
request: TokenLogic.showLoginScreen, | |
effect: { request in | |
request | |
.flatMapFirst(presentScene(animated: true) { | |
UIAlertController(title: "Login", message: "Enter your login info:", preferredStyle: .alert) | |
.scene { $0.connectLogin().map { TokenLogic.Input.login($0) } } | |
}) | |
} | |
) | |
) | |
.share() | |
let pages = cycle( | |
inputs: PageLogic.inputs( | |
reachedBottom: tableView.rx.reachedBottom().asObservable(), | |
token: token | |
), | |
initialState: PageLogic.State(), | |
reduce: PageLogic.reducer, | |
reaction: reaction( | |
request: PageLogic.requestInput, | |
effect: { request in | |
request.flatMapLatest { request in | |
api.response(.passenger(page: request.page, size: 20, token: request.token)) | |
.map { PageLogic.Input.response(page: request.page, response: $0) } | |
} | |
}) | |
) | |
token | |
.map { $0.isEmpty ? "Login" : "Logout" } | |
.bind(to: logoutButton.rx.title(for: .normal)) | |
.disposed(by: disposeBag) | |
Observable.merge( | |
pages.map { output in | |
output.pages.sorted { $0.key < $1.key } | |
.flatMap { $0.value } | |
}, | |
token.map(to: []) | |
) | |
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, item, cell in | |
cell.textLabel?.text = item.name | |
} | |
.disposed(by: disposeBag) | |
} | |
} | |
enum TokenLogic { | |
enum Input { | |
case logout | |
case login(Credentials) | |
} | |
static func reducer(state: inout String, input: Input) { | |
switch input { | |
case .logout: | |
state = "" | |
case let .login(credentials): | |
state = credentials.password | |
} | |
} | |
static func showLoginScreen(state: String, input: Input) -> Bool { | |
guard case .logout = input, state == "" else { return false } | |
return true | |
} | |
} | |
enum PageLogic { | |
enum Input { | |
case updateToken(String) | |
case getPage | |
case response(page: Int, response: PassengersResponse) | |
} | |
struct State { | |
var pages: [Int: [Passenger]] = [:] | |
var token: String = "" | |
} | |
struct RequestInput { | |
let page: Int | |
let token: String | |
} | |
static func inputs(reachedBottom: Observable<Void>, token: Observable<String>) -> [Observable<Input>] { | |
[ | |
reachedBottom.map(to: Input.getPage), | |
token.map(Input.updateToken) | |
] | |
} | |
static func reducer(state: inout State, input: Input) { | |
switch input { | |
case let .updateToken(token): | |
state.pages = [:] | |
state.token = token | |
case .getPage: | |
break | |
case .response(page: let page, response: let response): | |
state.pages[page] = response.data | |
} | |
} | |
static func requestInput(state: State, input: Input) -> RequestInput? { | |
switch input { | |
case let .updateToken(token): | |
guard !token.isEmpty else { return nil } | |
return RequestInput(page: 1, token: token) | |
case .getPage: | |
guard !state.token.isEmpty else { return nil } | |
return RequestInput(page: (state.pages.keys.sorted().last ?? 0) + 1, token: state.token) | |
case .response: | |
return nil | |
} | |
} | |
} | |
extension UIAlertController { | |
func connectLogin() -> Observable<Credentials> { | |
let credentials = PublishSubject<Credentials>() | |
let ok = UIAlertAction(title: "OK", style: .default, handler: { [unowned self] _ in | |
let username = self.textFields![0].text ?? "" | |
let password = self.textFields![0].text ?? "" | |
credentials.onSuccess(Credentials(username: username, password: password)) | |
}) | |
let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { _ in | |
credentials.onCompleted() | |
}) | |
addAction(cancel) | |
addAction(ok) | |
addTextField { (textField) in | |
textField.placeholder = "Username" | |
} | |
addTextField { (textField) in | |
textField.placeholder = "Password" | |
} | |
_ = Observable.combineLatest(textFields!.map { $0.rx.text.orEmpty }) | |
.map { $0.allSatisfy { !$0.isEmpty } } | |
.bind(to: ok.rx.isEnabled) | |
return credentials | |
} | |
} | |
extension Endpoint where Response == PassengersResponse { | |
static func passenger(page: Int, size: Int, token: String) -> Endpoint { | |
assert(!token.isEmpty) | |
var urlComponents = URLComponents(string: "https://api.instantwebtools.net/v1/passenger")! | |
urlComponents.queryItems = [ | |
URLQueryItem(name: "page", value: "\(page)"), | |
URLQueryItem(name: "size", value: "\(size)") | |
] | |
var request = URLRequest(url: urlComponents.url!) | |
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") | |
return Endpoint(request: request, decoder: JSONDecoder()) | |
} | |
} | |
struct Credentials { | |
let username: String | |
let password: String | |
} | |
struct PassengersResponse: Decodable, Equatable { | |
let data: [Passenger] | |
} | |
struct Passenger: Decodable, Identifiable, Equatable { | |
let id: Identifier<String, Passenger> | |
let name: String | |
enum CodingKeys: String, CodingKey { | |
case id = "_id" | |
case name | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment