Slide 1

Slide 1 text

iOSDC 2018 The things we’ve learned from iOS×React Native hybrid development @hotchemi

Slide 2

Slide 2 text

Goal: walk through hybrid style development in 15 mins!

Slide 3

Slide 3 text

01 02 03 04 05 Agenda | Introduction Motivation How we integrated Tips & Tricks Conclusion

Slide 4

Slide 4 text

01 Introduction

Slide 5

Slide 5 text

Introduction ● Shintaro Katafuchi ○ @hotchemi ● Engineering Manager@Quipper ○ StudySapuri ● Co-host of dex.fm(Android, React Native, Flutter) ○ Check ep55, 56

Slide 6

Slide 6 text

02 Motivation

Slide 7

Slide 7 text

React Native ● Build native mobile apps using JavaScript and React ○ “Learn Once, Write Anywhere” ● User: Facebook, Instagram, Skype, Microsoft, Discord ○ Quipper started investigation from Autumn 2017

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

Why we chose? ● To secure enough amount of mobile developers ○ Almost all web developer in Quipper can write React ● Share some logics/UI components among Web and App ● Easy to integrate with existing app

Slide 10

Slide 10 text

03 How we integrated

Slide 11

Slide 11 text

How we integrated ● Investigated by developing new small app ● Read “Integration with Existing Apps” section in official doc ● Migrated to “monorepo” ● Proceed a migration incrementally from new feature ● The magic API: RCTRootView ○ We wrap with UIViewController(ReactViewController)

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

● Surrounded: RCTRootView×TypeScript ● Others: (Pure)UIKit×Swift

Slide 14

Slide 14 text

public typealias ReactProps = [String : AnyObject] class ReactViewController: UIViewController { var screenName: String? var props: ReactProps? convenience init(screenName: String, props: ReactProps? = nil) { self.init(nibName: nil, bundle: nil) self.props = props } override func viewDidLoad() { super.viewDidLoad() populateReactView() } private func populateReactView() { guard let reactView = RCTRootView(bridge: ReactNative.shared.bridge, moduleName: screenName, initialProperties: nil) else { return } view.addSubview(reactView) reactView.translatesAutoresizingMaskIntoConstraints = false if #available(iOS 11.0, *) { let guide = self.view.safeAreaLayoutGuide reactView.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true reactView.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true reactView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true reactView.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true } }

Slide 15

Slide 15 text

class ProfileContainer extends Component { render() { return ( { return ( }} renderSectionHeader={({ section }) => { return ; }} sections={profileSections(this.props.user)} keyExtractor={item => item.name} stickySectionHeadersEnabled={true} /> ); } }

Slide 16

Slide 16 text

export default function registerComponents(store: Store) { AppRegistry.registerComponent(Screens.profile, () => withReduxStore(ProfileContainer, store), ); }

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

● TypeScript: 25% ● Swift: 75%

Slide 19

Slide 19 text

04 Tips&Tricks

Slide 20

Slide 20 text

Tips 01: Sunset Android support ● Just dropped 2 months ago ○ NDK, slow in debug mode, lots of workarounds are needed ○ Especially on “brown-field” app ● Cross platform is apparently difficult ○ Who knows both platform philosophies well? ○ Few developers can write a bridge

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

Tips 02: Data Management ● We basically use Redux but it can’t be genuine “Single Store” ○ Because existing code already has its own infrastructure ● Don’t sync any data as much as you can ○ Follow single-way sync direction if you really need to do ○ We only sync essential user data(like userID) ○ The magic API: RCTBridge, RCTDeviceEventEmitter

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

final class ReactNative: NSObject { enum EventType: String { case sendUserData } static let shared = ReactNative() var bridge: RCTBridge? private override init() { super.init() } func enqueueEvent(name: EventType, with: Any = []) { let args: [Any] = [name.rawValue as Any, with] bridge?.enqueueJSCall("RCTDeviceEventEmitter.emit", args: args) } }

Slide 25

Slide 25 text

func activate() -> Promise { return API.Qlearn.Bootstrap .bootstrap() .then { bootstrap -> Promise in ReactNative.shared.enqueueEvent(name: .sendUserData, with: bootstrap.toJSONString()) let initializer = Initializer() initializer.execute(user: user, context: userSessionContext) return Promise(resolved: bootstrap) } }

Slide 26

Slide 26 text

import { DeviceEventEmitter } from 'react-native'; import { Store } from 'redux'; import { BootStrap, updateBootStrap } from '../actions/bootstrapAction'; import { clearStore } from '../auth/authAction'; import { EventType } from '../constants/eventType'; export const observeSendUserDataEvent = (store: Store) => { DeviceEventEmitter.addListener(EventType.sendUserData, (payload: string) => { const bootstrap: BootStrap = JSON.parse(payload); store.dispatch(updateBootStrap(bootstrap)); }); };

Slide 27

Slide 27 text

Tips 03: UI Consistency ● Recognize React Native as “A customView written in JavaScript” ○ Use UIViewController for container/screen transition ○ Easy to integrate with existing screens ○ Easy to deal with new platform change(like SafeArea) ● Still important to follow HIG/iOS standard way

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

final class ProfileEditViewController: ReactViewController { var resolver: Resolver! override func viewDidLoad() { super.viewDidLoad() } @objc func onSave(_ sender: UIBarButtonItem) { ReactNative.shared.enqueueEvent(name: .onSaveProfileEdit) } @objc func onCancel(_ sender: UIBarButtonItem) { ReactNative.shared.enqueueEvent(name: .onCancelPress) } }

Slide 30

Slide 30 text

class ProfileEditContainer extends Component { private emitterSubscriptions: EmitterSubscription[] = []; constructor(props: Props) { super(props); this.setupEventEmitterListeners(); } setupEventEmitterListeners = () => { this.emitterSubscriptions.push( DeviceEventEmitter.addListener(EventType.onSaveProfileEdit, this.updateUserEvent); ); this.emitterSubscriptions.push( DeviceEventEmitter.addListener(EventType.onCancelPress, () => this.props.resetDraftUser(), ), ); }; }

Slide 31

Slide 31 text

05 Conclusion

Slide 32

Slide 32 text

Retrospective -Good- ● We could scale up iOS team ○ 2 Web devs, 1 iOS dev ● React component and redux are loose coupling and testable ● Performance is better than expected and no memory issue for now ● We can share some logics/interfaces(not UI)

Slide 33

Slide 33 text

Retrospective -Bad- ● Damn on Android! ● Not low learning curve for iOS dev ● Two style architectures are sort of of mixed ○ Need to know both Swift(ObjC) and JavaScript worlds

Slide 34

Slide 34 text

Hybrid is the way to go? ● First of all React Native is not a silver bullet ● you can scale up a team by mixing web practice and iOS knowledge ● If you develop “green field” app, Go! ● If you develop “brown field” app, be careful for aforementioned points

Slide 35

Slide 35 text

Thank you! @hotchemi