The things we’ve learned from iOS×React Native hybrid development

Ce37cf75fa85b89a33916545978c64de?s=47 @hotchemi
August 31, 2018

The things we’ve learned from iOS×React Native hybrid development

Ce37cf75fa85b89a33916545978c64de?s=128

@hotchemi

August 31, 2018
Tweet

Transcript

  1. iOSDC 2018 The things we’ve learned from iOS×React Native hybrid

    development @hotchemi
  2. Goal: walk through hybrid style development in 15 mins!

  3. 01 02 03 04 05 Agenda | Introduction Motivation How

    we integrated Tips & Tricks Conclusion
  4. 01 Introduction

  5. Introduction • Shintaro Katafuchi ◦ @hotchemi • Engineering Manager@Quipper ◦

    StudySapuri • Co-host of dex.fm(Android, React Native, Flutter) ◦ Check ep55, 56
  6. 02 Motivation

  7. 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
  8. None
  9. 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
  10. 03 How we integrated

  11. 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)
  12. None
  13. • Surrounded: RCTRootView×TypeScript • Others: (Pure)UIKit×Swift

  14. public typealias ReactProps = [String : AnyObject] class ReactViewController<BridgeModule: ReactNativeProxyBridgeModule>:

    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 } }
  15. class ProfileContainer extends Component<Props> { render() { return ( <SectionList

    renderItem={({ item }) => { return ( <SectionListItem item={item} onPress={onSwitchAccountPress} /> }} renderSectionHeader={({ section }) => { return <SectionListHeader section={section} />; }} sections={profileSections(this.props.user)} keyExtractor={item => item.name} stickySectionHeadersEnabled={true} /> ); } }
  16. export default function registerComponents(store: Store<any>) { AppRegistry.registerComponent(Screens.profile, () => withReduxStore(ProfileContainer,

    store), ); }
  17. None
  18. • TypeScript: 25% • Swift: 75%

  19. 04 Tips&Tricks

  20. 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
  21. None
  22. 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
  23. None
  24. 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) } }
  25. func activate() -> Promise<Qlearn.Bootstrap> { return API.Qlearn.Bootstrap .bootstrap() .then {

    bootstrap -> Promise<Qlearn.Bootstrap> in ReactNative.shared.enqueueEvent(name: .sendUserData, with: bootstrap.toJSONString()) let initializer = Initializer() initializer.execute(user: user, context: userSessionContext) return Promise<Qlearn.Bootstrap>(resolved: bootstrap) } }
  26. 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<any>) => { DeviceEventEmitter.addListener(EventType.sendUserData, (payload: string) => { const bootstrap: BootStrap = JSON.parse(payload); store.dispatch(updateBootStrap(bootstrap)); }); };
  27. 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
  28. None
  29. final class ProfileEditViewController: ReactViewController<ProfileEditViewBridgeModule> { 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) } }
  30. class ProfileEditContainer extends Component<Props, State> { 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(), ), ); }; }
  31. 05 Conclusion

  32. 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)
  33. 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
  34. 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
  35. Thank you! @hotchemi