Skip to content

Commit b5eb0fa

Browse files
committed
Initial commit :)
0 parents  commit b5eb0fa

File tree

12 files changed

+520
-0
lines changed

12 files changed

+520
-0
lines changed

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.DS_Store
2+
sourcekitten-output.json
3+
docs/
4+
/.build
5+
/Packages
6+
/*.xcodeproj
7+
**/xcuserdata
8+
**/xcshareddata
9+
Pods/
10+
Carthage/
11+
Examples/**/Podfile.lock

AlertReactor.podspec

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Pod::Spec.new do |s|
2+
s.name = "AlertReactor"
3+
s.version = "0.1.0"
4+
s.summary = "ReactorKit extension for UIAlertController"
5+
s.homepage = "https://github.com/devxoul/AlertReactor"
6+
s.license = { :type => "MIT", :file => "LICENSE" }
7+
s.author = { "Suyeol Jeon" => "[email protected]" }
8+
s.source = { :git => "https://github.com/devxoul/AlertReactor.git",
9+
:tag => s.version.to_s }
10+
s.source_files = "Sources/**/*.{swift,h,m}"
11+
s.frameworks = "Foundation"
12+
s.dependency "ReactorKit"
13+
14+
s.ios.deployment_target = "8.0"
15+
s.osx.deployment_target = "10.11"
16+
s.tvos.deployment_target = "9.0"
17+
s.watchos.deployment_target = "2.0"
18+
19+
s.pod_target_xcconfig = {
20+
"SWIFT_VERSION" => "3.1"
21+
}
22+
end

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Suyeol Jeon (xoul.kr)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Package.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// swift-tools-version:3.1
2+
3+
import Foundation
4+
import PackageDescription
5+
6+
var dependencies: [Package.Dependency] = [
7+
.Package(url: "https://github.com/ReactiveX/RxSwift.git", majorVersion: 3),
8+
.Package(url: "https://github.com/ReactorKit/ReactorKit.git", majorVersion: 0),
9+
]
10+
11+
let isTest = ProcessInfo.processInfo.environment["TEST"] == "1"
12+
if isTest {
13+
dependencies.append(
14+
.Package(url: "https://github.com/devxoul/RxExpect.git", majorVersion: 0)
15+
)
16+
}
17+
18+
let package = Package(
19+
name: "AlertReactor",
20+
dependencies: dependencies
21+
)

README.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# AlertReactor
2+
3+
![Swift](https://img.shields.io/badge/Swift-3.1-orange.svg)
4+
[![CocoaPods](http://img.shields.io/cocoapods/v/AlertReactor.svg)](https://cocoapods.org/pods/AlertReactor)
5+
[![Build Status](https://travis-ci.org/devxoul/AlertReactor.svg?branch=master)](https://travis-ci.org/devxoul/AlertReactor)
6+
[![Codecov](https://img.shields.io/codecov/c/github/devxoul/AlertReactor.svg)](https://codecov.io/gh/devxoul/AlertReactor)
7+
8+
ReactorKit extension for UIAlertController. It provides an elegant way to deal with an UIAlertController. Best fits for lazy-loaded alert actions.
9+
10+
## Features
11+
12+
* Statically typed alert actions
13+
* Reactive and dynamic action bindings
14+
15+
## At a Glance
16+
17+
With AlertReactor, you can write a reactive code for alert controller. The code below displays an action sheet when `menuButton` is tapped. When an user selects an item in the action sheet, the selected menu item is converted into an action which is binded to a reactor.
18+
19+
```swift
20+
// Menu Button -> Action Sheet -> Reactor Action
21+
menuButton.rx.tap
22+
.flatMap { [weak self] _ -> Observable<UserAlertAction> in
23+
let reactor = UserAlertReactor()
24+
let controller = AlertController<UserAlertAction>(reactor: reactor, preferredStyle: .actionSheet)
25+
self?.present(controller, animated: true, completion: nil)
26+
return controller.rx.actionSelected.asObservable()
27+
}
28+
.map { alertAction -> Reactor.Action? in
29+
switch action {
30+
case .follow: return .followUser
31+
case .unfollow: return .unfollowUser
32+
case .block: return .blockUser
33+
case .cancel: return nil
34+
}
35+
}
36+
.filterNil()
37+
.bind(to: reactor.action)
38+
```
39+
40+
## Getting Started
41+
42+
### 1. Defining an Alert Action
43+
44+
AlertReactor provides a `AlertActionType ` protocol. This is an abstraction model of `UIAlertAction`. Create a new type conforming this protocol. This protocol requires a `title` and `style` property.
45+
46+
```swift
47+
enum UserAlertAction: AlertActionType {
48+
case follow
49+
case unfollow
50+
case block
51+
case cancel
52+
53+
// required
54+
var title: String {
55+
case follow: return "Follow"
56+
case unfollow: return "Unfollow"
57+
case block: return "Block"
58+
case cancel: return "Cancel"
59+
}
60+
61+
// optional
62+
var style: UIAlertActionStyle {
63+
case follow: return .default
64+
case unfollow: return .default
65+
case block: return .destructive
66+
case cancel: return .cancel
67+
}
68+
}
69+
```
70+
71+
72+
### 2. Creating an Alert Reactor
73+
74+
`AlertReactor` is a reactor class. It has an action, mutation and state:
75+
76+
```swift
77+
enum Action {
78+
case prepare
79+
}
80+
81+
enum Mutation {
82+
case setTitle(String?)
83+
case setMessage(String?)
84+
case setActions([AlertAction])
85+
}
86+
87+
struct State {
88+
public var title: String?
89+
public var message: String?
90+
public var actions: [AlertAction]
91+
}
92+
```
93+
94+
Override this class to implement `mutate(action:)` method so that the reactor can emit mutations to change the state. Here is an example of lazy-loaded actions.
95+
96+
```swift
97+
final class UserAlertReactor: AlertReactor<UserAlertAction> {
98+
let userID: Int
99+
100+
init(userID: Int) {
101+
self.userID = userID
102+
}
103+
104+
override func mutate(action: Action) -> Observable<Mutation> {
105+
return Observable.concat([
106+
// Initial actions
107+
Observable.just(Mutation.setTitle("Loading...")),
108+
Observable.just(Mutation.setActions([.block, .cancel])),
109+
110+
// Call API to choose follow or unfollow
111+
api.isFollowing(userID: userID)
112+
.map { isFollowing -> Mutation in
113+
if isFollowing {
114+
return Mutation.setActions([.unfollow, .block, .cancel])
115+
} else {
116+
return Mutation.setActions([.follow, .block, .cancel])
117+
}
118+
}
119+
Observable.just(Mutation.setTitle(nil)),
120+
])
121+
}
122+
}
123+
```
124+
125+
### 3. Using with Alert Controller
126+
127+
A generic class `AlertController` is provided. You can create it with some parameters: `reactor` and `preferredStyle`. There's also a `actionSelected` control event property in a reactive extension.
128+
129+
```swift
130+
let reactor = UserAlertReactor(userID: 12345)
131+
let controller = AlertController<UserAlertAction>(reactor: reactor, preferredStyle: .actionSheet)
132+
controller.rx.actionSelected
133+
.subscribe(onNext: { action in
134+
switch action {
135+
case .follow: print("Follow user")
136+
case .unfollow: print("Unfollow user")
137+
case .block: print("Block user")
138+
case .cancel: print("Cancel")
139+
}
140+
})
141+
```
142+
143+
## Installation
144+
145+
```ruby
146+
pod 'AlertReactor'
147+
```
148+
149+
## License
150+
151+
AlertReactor is under MIT license. See the [LICENSE](LICENSE) for more info.

Sources/AlertActionType.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import UIKit
2+
3+
public protocol AlertActionType: Equatable {
4+
var title: String { get }
5+
var style: UIAlertActionStyle { get }
6+
}
7+
8+
public extension AlertActionType {
9+
var style: UIAlertActionStyle {
10+
return .default
11+
}
12+
}

Sources/AlertController.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import UIKit
2+
3+
import ReactorKit
4+
import RxCocoa
5+
import RxSwift
6+
7+
8+
// MARK: - AlertControllerType
9+
10+
public protocol AlertControllerType: class {
11+
associatedtype AlertAction: AlertActionType
12+
var _actionSelectedSubject: PublishSubject<AlertAction> { get }
13+
func setValue(_ value: Any?, forKey key: String)
14+
}
15+
16+
17+
// MARK: - AlertController
18+
19+
open class AlertController<A: AlertActionType>: UIAlertController, AlertControllerType, View {
20+
public typealias AlertAction = A
21+
22+
23+
// MARK: Properties
24+
25+
open var disposeBag = DisposeBag()
26+
public private(set) lazy var _actionSelectedSubject: PublishSubject<AlertAction> = .init()
27+
28+
private var _preferredStyle: UIAlertControllerStyle
29+
open override var preferredStyle: UIAlertControllerStyle {
30+
get { return self._preferredStyle }
31+
set { self._preferredStyle = newValue }
32+
}
33+
34+
35+
// MARK: Initializing
36+
37+
public init(reactor: AlertReactor<AlertAction>? = nil, preferredStyle: UIAlertControllerStyle = .alert) {
38+
self._preferredStyle = preferredStyle
39+
super.init(nibName: nil, bundle: nil)
40+
self.title = ""
41+
self.reactor = reactor
42+
}
43+
44+
public required init?(coder aDecoder: NSCoder) {
45+
fatalError("init(coder:) has not been implemented")
46+
}
47+
48+
49+
// MARK: Binding
50+
51+
open func bind(reactor: AlertReactor<AlertAction>) {
52+
// Action
53+
self.rx.methodInvoked(#selector(UIViewController.viewDidLoad))
54+
.map { _ in Reactor.Action.prepare }
55+
.bind(to: reactor.action)
56+
.disposed(by: self.disposeBag)
57+
58+
// State
59+
reactor.state.map { $0.title }
60+
.distinctUntilChanged { $0 == $1 }
61+
.subscribe(onNext: { [weak self] title in
62+
self?.title = title
63+
})
64+
.disposed(by: self.disposeBag)
65+
66+
reactor.state.map { $0.message }
67+
.distinctUntilChanged { $0 == $1 }
68+
.subscribe(onNext: { [weak self] message in
69+
self?.message = message
70+
})
71+
.disposed(by: self.disposeBag)
72+
73+
reactor.state.map { $0.actions }
74+
.distinctUntilChanged { old, new in
75+
old.elementsEqual(new) { $0.title == $1.title && $0.style == $1.style }
76+
}
77+
.bind(to: self.rx.actions)
78+
.disposed(by: self.disposeBag)
79+
}
80+
}
81+
82+
83+
// MARK: - Reactive Extension
84+
85+
extension Reactive where Base: AlertControllerType {
86+
var actions: UIBindingObserver<Base, [Base.AlertAction]> {
87+
return UIBindingObserver(UIElement: self.base) { alertController, actions in
88+
let alertActions = actions.map { action in
89+
UIAlertAction(title: action.title, style: action.style) { [weak base = self.base] _ in
90+
base?._actionSelectedSubject.onNext(action)
91+
}
92+
}
93+
alertController.setValue(alertActions, forKey: "actions")
94+
}
95+
}
96+
97+
var actionSelected: ControlEvent<Base.AlertAction> {
98+
let source = self.base._actionSelectedSubject.asObservable()
99+
return ControlEvent(events: source)
100+
}
101+
}

0 commit comments

Comments
 (0)