Slide 1

Slide 1 text

RxSwiftΛಋೖͨͯ͘͠΋ Ͱ͖ͳ͔ͬͨΒʁ CA.swift 2017/03/01 marty-suzuki

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ͳͥRxSwiftΛ ಋೖͰ͖ͳ͔ͬͨͷ͔ʁ Why can not we use RxSwift?

Slide 5

Slide 5 text

Problem 1 App Size 57.2MB (iPhone7)

Slide 6

Slide 6 text

ࠓޙ΍Δ͜ͱ͕ܾ·͍ͬͯΔ͜ͱ • େܕͷ௥Ճػೳ • Firebaseͷಋೖ

Slide 7

Slide 7 text

ࠓޙ΍Δ͜ͱ͕ܾ·͍ͬͯΔ͜ͱ • େܕͷ௥Ճػೳ • Firebaseͷಋೖ Wi-Fi؀ڥͰͳͯ͘΋ΞϓϦ͕μ΢ϯϩʔυͰ͖ Δ༰ྔʢ100MBʣΛӽ͑ͯ͠·Θͳ͍͔͕৺഑

Slide 8

Slide 8 text

Problem 2 Application Sources

Slide 9

Slide 9 text

Objective-C++ 45.0% Swift 27.6% Objective-C 27.4%

Slide 10

Slide 10 text

1155 Compile Sources

Slide 11

Slide 11 text

Architecture MVC + KVO

Slide 12

Slide 12 text

• Swift͕3ׂऑͰͳͷͰɺRxSwiftΛద༻Ͱ͖ΔՕॴ͕গͳ͍ • RxSwiftΛ࢖ͬͨMVVMͷઃܭΛہॴతʹ௥Ճͯ͠͠·͏ͱɺ Objective-C / C++ɺSwift͕ࠞࡏ͍ͯ͠Δ࣌఺Ͱෳࡶͳͷʹ ΋ؔΘΒͣ͞Βʹෳࡶ͕͞૿ͯ͠͠·͍ɺҾ͖ܧ͗ʹ͍͘ι ʔεʹͳͬͯ͠·͏

Slide 13

Slide 13 text

Other Solutions

Slide 14

Slide 14 text

Text Input

Slide 15

Slide 15 text

UISearchBar + Debounce searchBar.rx.text.orEmpty.asDriver() .skip(1) .debounce(0.3) .distinctUntilChanged() .drive(onNext: { text in print(text) }) .addDisposableTo(disposeBag)

Slide 16

Slide 16 text

UISearchBar + Timer class ViewController: UIViewController, UISearchBarDelegate { @IBOutlet weak var searchBar: UISearchBar! private lazy var timer: Timer = { return self.createTimer() }() override func viewDidLoad() { super.viewDidLoad() searchBar.delegate = self } func createTimer() -> Timer { return Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(ViewController.handleTimer(_:)), userInfo: nil, repeats: false) } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { timer.invalidate() timer = createTimer() } @objc private func handleTimer(_ timer: Timer) { print(self.searchBar.text) } }

Slide 17

Slide 17 text

Text Input Solution

Slide 18

Slide 18 text

UISearchBar + DispatchQueue class ViewController: UIViewController, UISearchBarDelegate { @IBOutlet weak var searchBar: UISearchBar! //0.5ඵ͝ͱʹॲཧΛglobalQueueͰߦ͏ClosureΛPropertyͱͯ͠อ࣋ let debounce = DispatchQueue.global().debounce(delay: .milliseconds(500)) override func viewDidLoad() { super.viewDidLoad() searchBar.delegate = self } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { debounce { print(searchBar.text) } } }

Slide 19

Slide 19 text

Implementation extension DispatchQueue { func debounce(delay: DispatchTimeInterval) -> (_ action: @escaping () -> ()) -> () { var lastFireTime: DispatchTime = .now() return { action in let deadline: DispatchTime = .now() + delay lastFireTime = .now() self.asyncAfter(deadline: deadline) { let now: DispatchTime = .now() let when: DispatchTime = lastFireTime + delay if now < when { return } lastFireTime = .now() action() } } } }

Slide 20

Slide 20 text

Notification Center

Slide 21

Slide 21 text

NotificationCenter + RxSwift class ViewController: UIViewController { //some properties private var disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.rx.notification(.UIKeyboardWillShow).asDriver(onErrorDriveWith: .empty()) .flatMap { notification -> Driver in guard let keyboardInfo = UIKeyboardNotificationInfo(from: notification) else { return Driver.empty() } return Driver.just(keyboardInfo) } .drive(onNext: { [unowned self] info in self.keyboardWillAnimate(toDefault: false, with: info) }) .addDisposableTo(disposeBag) } func keyboardWillAnimate(toDefault: Bool, with info: UIKeyboardNotificationInfo) { //Keyboard animation } }

Slide 22

Slide 22 text

Notification Center Solution

Slide 23

Slide 23 text

NoticeObserveKit https://github.com/marty-suzuki/NoticeObserveKit class ViewController: UIViewController { //some properties private var pool = NoticeObserverPool() override func viewDidLoad() { super.viewDidLoad() UIKeyboardWillShow.observe { [unowned self] in self.keyboardWillAnimate(toDefault: false, with: $0) }.addObserverTo(pool) } }

Slide 24

Slide 24 text

NoticeObserveKit.NoticeType struct UIKeyboardWillShow: NoticeType { typealias InfoType = UIKeyboardInfo static let name: Notification.Name = .UIKeyboardWillShow }

Slide 25

Slide 25 text

NoticeObserveKit.NoticeType struct TalkObjectDidUpdate: NoticeType { typealias InfoType = TalkObject static let name: Notification.Name = Notification.Name("TalkObjectDidUpdate") } let talk: TalkObject = //... TalkObjectDidUpdate.post(info: talk)

Slide 26

Slide 26 text

Paging

Slide 27

Slide 27 text

RxPagination https://github.com/tryswift/RxPagination extension Reactive where Base: UIScrollView { var reachedBottom: ControlEvent { let observable = contentOffset .flatMap { [weak base] contentOffset -> Observable in guard let scrollView = base else { return Observable.empty() } let visibleHeight = scrollView.frame.height - scrollView.contentInset.top - scrollView.contentInset.bottom let y = contentOffset.y + scrollView.contentInset.top let threshold = max(0.0, scrollView.contentSize.height - visibleHeight) return y > threshold ? Observable.just() : Observable.empty() } return ControlEvent(events: observable) } }

Slide 28

Slide 28 text

Paging + UITableViewDelegate class ViewController: UIViewController, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! private var reachedBottom: Bool = false { didSet { if reachedBottom == oldValue { return } guard reachedBottom else { return } didReachBottom() } } override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self } private func didReachBottom() { print("scrollViewDidReachBottom!") } func scrollViewDidScroll(_ scrollView: UIScrollView) { let maxDistance = max(0, scrollView.contentSize.height - scrollView.bounds.size.height) reachedBottom = maxDistance < scrollView.contentOffset.y } }

Slide 29

Slide 29 text

Paging Solution

Slide 30

Slide 30 text

Paging + ReachExtension class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() tableView.re.delegate = self tableView.re.scrollViewDidReachBottom = { scrollView in print("scrollViewDidReachBottom!") } } } extension ViewController: UITableViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { print("scrollViewDidScroll = \(scrollView.contentOffset.y)") } }

Slide 31

Slide 31 text

Implementation

Slide 32

Slide 32 text

DelegateTransporter.h @interface DelegateTransporter : NSObject - (nonnull instancetype)initWithDelegates:(NSArray * __nonnull)delegates NS_REFINED_FOR_SWIFT; @end

Slide 33

Slide 33 text

DelegateTransporter.m @interface DelegateTransporter () @property (nonnull, nonatomic, strong) NSHashTable *delegates; @end @implementation DelegateTransporter - (instancetype)initWithDelegates:(NSArray *)delegates { self = [super init]; if (self != nil) { self.delegates = [NSHashTable weakObjectsHashTable]; for (id delegate in delegates) { if (![delegates isKindOfClass:[NSObject class]]) { continue; } [self.delegates addObject:delegate]; } } return self; } //...etc @end

Slide 34

Slide 34 text

DelegateTransporter.m - (BOOL)respondsToSelector:(SEL)aSelector { for (NSObject *delegate in self.delegates) { if ([delegate isKindOfClass:[NSNull class]]) { continue; } if ([delegate respondsToSelector:aSelector]) { return YES; } } return NO; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { for (NSObject *delegate in self.delegates) { if ([delegate isKindOfClass:[NSNull class]]) { continue; } if ([delegate respondsToSelector:aSelector]) { return [delegate methodSignatureForSelector:aSelector]; } } return nil; } - (void)forwardInvocation:(NSInvocation *)anInvocation { for (NSObject *delegate in self.delegates) { if ([delegate isKindOfClass:[NSNull class]]) { continue; } if ([delegate respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:delegate]; } } }

Slide 35

Slide 35 text

UITableViewDelegateTransporter class UITableViewDelegateTransporter: DelegateTransporter, UITableViewDelegate { @nonobjc convenience init(delegates: [UITableViewDelegate]) { self.init(__delegates: delegates) } }

Slide 36

Slide 36 text

UITableView.ReachExtension extension UITableView { final class ReachExtension: NSObject { private var delegateTransporter: UITableViewDelegateTransporter? { didSet { base?.delegate = delegateTransporter } } weak var delegate: UITableViewDelegate? { didSet { guard let delegate = delegate else { delegateTransporter = nil return } delegateTransporter = UITableViewDelegateTransporter(delegates: [delegate, self]) } } fileprivate var reachedBottom: Bool = false { didSet { if reachedBottom == oldValue { return } guard let base = base, reachedBottom else { return } scrollViewDidReachBottom?(base) } } var scrollViewDidReachBottom: ((UIScrollView) -> ())? private weak var base: UITableView? init(_ base: UITableView) { self.base = base super.init() } } }

Slide 37

Slide 37 text

UITableView.ReachExtension extension UITableView.ReachExtension: UITableViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { let maxDistance = max(0, scrollView.contentSize.height - scrollView.bounds.size.height) reachedBottom = maxDistance < scrollView.contentOffset.y } }

Slide 38

Slide 38 text

UITableView extension UITableView { private struct AssociatedKey { static var re: UInt8 = 0 } var re: ReachExtension { guard let re = objc_getAssociatedObject(self, &AssociatedKey.re) as? ReachExtension else { let re = ReachExtension(self) objc_setAssociatedObject(self, &AssociatedKey.re, re, .OBJC_ASSOCIATION_RETAIN) return re } return re } }

Slide 39

Slide 39 text

Advanced Usage

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

UITableView.ReverseExtension https://github.com/marty-suzuki/ReverseExtension extension UITableView { final class ReverseExtension: NSObject { //some implementation //... private var lastScrollIndicatorInsets: UIEdgeInsets? private var lastContentInset: UIEdgeInsets? private var mutex = pthread_mutex_t() init(_ base: UITableView) { self.base = base super.init() configureTableView(base) } //some implementation //... } }

Slide 46

Slide 46 text

UITableView.ReverseExtension private func configureTableView(_ tableView: UITableView) { guard let base = self.base else { return } if base.transform == CGAffineTransform.identity { UIView.setAnimationsEnabled(false) base.transform = CGAffineTransform.identity.rotated(by: .pi) UIView.setAnimationsEnabled(true) } tableView.addObserver(self, forKeyPath: #keyPath(UITableView.contentInset), options: [.new, .old], context: nil) } public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { switch keyPath { case (#keyPath(UITableView.contentInset))?: DispatchQueue.global().async { [weak self] in DispatchQueue.main.async { self?.configureTableViewInsets() } } default: break } }

Slide 47

Slide 47 text

UITableView.ReverseExtension private func configureTableViewInsets() { defer { pthread_mutex_unlock(&mutex) } pthread_mutex_lock(&mutex) guard let base = base else { return } if let _ = self.lastContentInset, let _ = self.lastScrollIndicatorInsets { return } let contentInset = base.contentInset base.contentInset.bottom = contentInset.top base.contentInset.top = contentInset.bottom self.lastContentInset = base.contentInset let scrollIndicatorInsets = base.scrollIndicatorInsets base.scrollIndicatorInsets.bottom = scrollIndicatorInsets.top base.scrollIndicatorInsets.top = scrollIndicatorInsets.bottom base.scrollIndicatorInsets.right = base.bounds.size.width - 8 self.lastScrollIndicatorInsets = base.scrollIndicatorInsets }

Slide 48

Slide 48 text

UITableView.ReverseExtension extension UITableView.ReverseExtension: UITableViewDelegate { func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if cell.transform == CGAffineTransform.identity { UIView.setAnimationsEnabled(false) cell.transform = CGAffineTransform.identity.rotated(by: .pi) UIView.setAnimationsEnabled(true) } } }

Slide 49

Slide 49 text

ϞόΠϧΞϓϦ։ൃΤΩεύʔτཆ੒ಡຊʢ4/11ൃചʣ https://www.amazon.co.jp/dp/4774188638

Slide 50

Slide 50 text

͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ marty-suzuki