I was working on my app to support tablets. On iPad, it has a multitasking feature that allows you to use two apps at the same time by splitting screen as below:
In React Native, it needs some hacks to support this feature because there is a problem where Dimensions
doesn't support it.
You always get the same data from Dimensions.get
even if with the app on "Split View" or "Slide Over" on iPad:
console.log(Dimensions.get('screen')) // {fontScale: 1, width: 768, height: 1024, scale: 2}
console.log(Dimensions.get('window')) // {fontScale: 1, width: 768, height: 1024, scale: 2}
So you need to get an actual window size somehow.
To accomplish it, you have to have a view the outermost of your views with flex: 1
style.
And set onLayout
event to get its size, and remember it somewhere like Redux store.
Adaptable Layout Provider / Consumer
Here is code snippets to easily support for split view on your app.
It takes provider-consumer pattern but doesn't depend on React's Context API because it stores the state to Redux store.
Provider
// @flow
// adaptable-layout-provider.js
import * as React from 'react'
import { View, StyleSheet } from 'react-native'
import { compose, withHandlers, pure, type HOC } from 'recompose'
import actions from '../actions'
import withDispatch from '../utils/with-dispatch'
/**
* <View onLayout={...} />
* <FlatList onLayout={...} /> (FlatList is just wrapper for View)
*
* @see https://facebook.github.io/react-native/docs/view.html#onlayout
*/
export type OnLayout = {|
nativeEvent: {|
layout: {|
x: number,
y: number,
width: number,
height: number
|}
|}
|}
type Props = {
children: React.Node
}
const enhance: HOC<*, Props> = compose(
withDispatch(),
pure,
withHandlers({
emitDimensionChanges: props => (event: OnLayout) => {
const { dispatch } = props
const { width, height } = event.nativeEvent.layout
dispatch(actions.viewport.update({ width, height }))
}
})
)
const Provider = enhance(props => (
<View style={styles.container} onLayout={props.emitDimensionChanges}>
{props.children}
</View>
))
export default Provider
const styles = StyleSheet.create({
container: {
flex: 1
}
})
Consumer
// @flow
// adaptable-layout-consumer.js
import * as React from 'react'
import { compose, pure, type HOC } from 'recompose'
import connect from '../utils/connect-store'
type Props = {
renderOnWide?: React.Node,
renderOnNarrow?: React.Node
}
const enhance: HOC<*, Props> = compose(
connect(({ viewport }) => ({ viewport })),
pure
)
const Consumer = enhance(props => {
const { viewport } = props
// may return nothing:
// 1. renderOnWide set but we have narrow layout
// 2. renderOnNarrow set but we have wide layout
let children = null
const wideLayout = viewport.isTablet
if (wideLayout === true && props.renderOnWide) {
children = props.renderOnWide
} else if (wideLayout === false && props.renderOnNarrow) {
children = props.renderOnNarrow
}
return children
})
export default Consumer
Reducer
// @flow
// reducers/viewport.js
import type { ViewportActionType } from '../actions/viewport'
import * as viewportActions from '../actions/viewport'
import { Dimensions } from 'react-native'
export type Dimension = {
width: number,
height: number
}
export type ViewportState = {
width: number,
height: number,
isLandscape: boolean,
isPortrait: boolean,
isTablet: boolean,
isPhone: boolean
}
function isLandscape(dim: Dimension) {
return dim.width >= dim.height
}
function isTablet(dim: Dimension) {
return dim.width >= 1024
}
const dim: Dimension = Dimensions.get('window')
export const initialViewportState: ViewportState = {
width: dim.width,
height: dim.height,
isLandscape: isLandscape(dim),
isPortrait: !isLandscape(dim),
isTablet: isTablet(dim),
isPhone: !isTablet(dim)
}
export default function viewport(
state: ViewportState = initialViewportState,
action: ViewportActionType
): ViewportState {
switch (action.type) {
case viewportActions.VIEWPORT_UPDATE:
const dim = action.payload
return {
...action.payload,
isLandscape: isLandscape(dim),
isPortrait: !isLandscape(dim),
isTablet: isTablet(dim),
isPhone: !isTablet(dim)
}
default:
return state || initialViewportState
}
}
Action
// @flow
import { type Dimension } from '../reducers/viewport'
export const VIEWPORT_UPDATE = 'VIEWPORT_UPDATE'
export type ViewportActionType = {
type: 'VIEWPORT_UPDATE',
payload: Dimension
}
export function update(dim: Dimension) {
return {
type: VIEWPORT_UPDATE,
payload: dim
}
}
In this example, it stores window size to Redux store.
However you can also store it in global variables, which I don't recommend though but it's just simple.
How to use it
In your root view component:
const RootView = () => (
<AdaptableLayoutProvider>
<MainScreen />
</AdaptableLayoutProvider>
)
In your screen component:
const MainScreen = props => {
return (
<AdaptableLayoutConsumer
renderOnNarrow={
<MobileLayout />
}
renderOnWide={
<ThreeColumnLayout />
}
/>
)
}
Hope that helps!
- Follow me on Twitter
- Read my blogposts more on Medium
Top comments (1)
Hi @Takuya I'm doing analysis about migrating some iPad app to react-native and I couldn't find a good documentation about this process, what you would say about working on an IPad app using React Native, the app that I'm reviewing has plenty of Animations and heavy use of Bluetooth for syncing between devices and a lot of graphical stuff. Thanks in advance!