leecade / react-native-swiper

The best Swiper component for React Native.
MIT License
10.4k stars 2.34k forks source link

dynamic+static swiper #233

Open juanluisgv1 opened 8 years ago

juanluisgv1 commented 8 years ago

I have a weird issue trying to use dynamic&static content in Swiper. Im not able to use both approaches to create the result I need.

My code is:

<Swiper horizontal={false}>
          <View> ...View 1 (static header)...</View>
          {this.items.map((item, key) => {
             return (
                  <View key={key} style={styles.slide1}>
                     <Text>bla bla</Text>
                  </View>
             )
            });
          }
          <View> ... View 3 (static footer) ...</View>
</Swiper>

This will produce something like:

View 1 (static header) / View 2 --> Contains 4 childviews where each view take 1/4 of the screen / View 3 (static footer)

On the other hand, if I delete header and footer and I leave the map function alone I will see the 4 views I'm expecting.... any idea?

Thanks! Juan

getnashty commented 8 years ago

Hi Juan,

Your options are to give each slide their own header/footer (swiper may need custom styles for this to work, or place the header/footer with absolute positioning):

<Swiper horizontal={false}>
          {this.items.map((item, key) => {
             return (
                  <View>
                    <View> ...View 1 (static header)...</View>
                    <View key={key} style={styles.slide1}>
                       <Text>bla bla</Text>
                    </View>
                    <View> ... View 3 (static footer) ...</View>
                  </View>
             )
            });
          }
</Swiper>

or, wrap the swiper in the header / footer:

<View style={{flex:1, flexDirection:'column'}}>
    <View> ...View 1 (static header)...</View>
    <Swiper horizontal={false}>
          {this.items.map((item, key) => {
             return (
                  <View key={key} style={styles.slide1}>
                     <Text>bla bla</Text>
                  </View>
             )
            });
          }
    </Swiper>
    <View> ... View 3 (static footer) ...</View>
</View>
danmatthews commented 7 years ago

I'm having a similar issue, but i don't need a header + footer as @getnashty does.

The slider has two 'introductory' slides first that set up titles and descriptions, then each step of the journey is printed out.

But unfortunately they print the slides from the loop out on the same page:

image

Code below:

journeyData = {
  "steps": [{id:1}, {id:2}, {id:3}, {id:4}]
};
return (
      <View style={{marginTop: 0,backgroundColor:'#252525'}}>
      <Swiper style={styles.wrapper} showsButtons={false} showsPagination={false} loop={false} bounces={true}>
      <View style={{marginTop: 40}}>
        <Text style={{color:'#ffffff', textAlign:'center', fontSize: 32, fontWeight:'bold', marginRight: 30, marginLeft: 30}}>{this.journeyData.title}</Text>
        <Text style={{color:'#ffffff', textAlign:'center', fontSize: 18, marginTop: 30, marginRight: 30, marginLeft: 30}}>{this.journeyData.description}</Text>
      </View>

      <View style={{marginTop: 40}}>
        <Text style={{color:'#ffffff', textAlign:'center', fontSize: 32, fontWeight:'bold', marginRight: 30, marginLeft: 30}}>Parking</Text>
        <Text style={{color:'#ffffff', textAlign:'center', fontSize: 18, marginTop: 30, marginRight: 30, marginLeft: 30}}>{this.journeyData.parking.description}</Text>

        <MapView
        style={{height: 200, margin: 40}}
        showsUserLocation={false}
        region={this.journeyData.parking.location}
        annotations={[{longitude: this.journeyData.parking.location.longitude, latitude: this.journeyData.parking.location.latitude, animateDrop: true, draggable: false, title:"Parking"}]}/>
      </View>

      {this.journeyData.steps.map(function(current, index) {
        return (<View key={index}>
          <Text style={{textAlign:'center', 'color':'#fff', fontSize:20,marginBottom:20}}>Route Step #{current.id}</Text>

        </View>)
      })}

    </Swiper>
      </View>
    );
juanluisgv1 commented 7 years ago

I had the same issue. I needed to create an array of views so I guess you could do sth like this:

this.toRender = [];
this.journeyData.steps.map(function(current, index) {
        this.toRender.push( () => return (<View key={index}>
          <Text style={{textAlign:'center', 'color':'#fff', fontSize:20,marginBottom:20}}>Route Step #{current.id}</Text>
        </View>))
      });
danmatthews commented 7 years ago

Then when you want to render them, how do you render the Array?

Like this: {this.toRender}

or like this: {this.toRender.join('')}

juanluisgv1 commented 7 years ago

I would do this:

<Swiper style={styles.wrapper} showsButtons={false} showsPagination={false} loop={false} bounces={true}>

      {this.toRender}

</Swiper>

In my case (I guess always) I couldn't have different Views out of the array.

danmatthews commented 7 years ago

Thank you! I'll give it a try tonight.

danmatthews commented 7 years ago

Damn - no luck, they still stack.

image

shawnpanda commented 7 years ago

I have been struggling with the same issue of rendering dynamic elements under one view. My views still stack in one view even after following @juanluisgv1's advice on creating an array of views. Would appreciate any guidance!

danmatthews commented 7 years ago

Bump - would love to know if anyone has any idea how this could be fixed?

Kottidev commented 7 years ago

+1

i have same probleme

{tags}
mehdipourfar commented 7 years ago

I have the same problem

okeeffed commented 7 years ago

Yo guys just had this issue at work and solved it by re-rendering the entire Swiper based on a Boolean - you can make this a little more DRY from here (which I'm about to do) but just posting so you guys know my work around...

_renderWithoutExpenses() {
        const sectionOneArray = this._renderSectionOne();
        const sectionThreeArray = this._renderSectionThree();

        const componentReturn = [...sectionOneArray, ...sectionThreeArray];
        const componentList = componentReturn.map((item) => item);

        return (
            <Swiper 
                ref='swiper'
                showsPagination={false}
                style={styles.swiper}
                loop={false}
                onMomentumScrollEnd = {this._onMomentumScrollEnd}
                height={this._updateBoundaries()}
                >
                    { componentList }
            </Swiper>
        );
    }

_renderWithExpenses() {
        const sectionOneArray = this._renderSectionOne();
        const sectionTwoArray = this._renderSectionTwo();
        const sectionThreeArray = this._renderSectionThree();

        const componentReturn = [...sectionOneArray, ...sectionTwoArray, ...sectionThreeArray];
        const componentList = componentReturn.map((item) => item);

        return (
            <Swiper 
                ref='swiper'
                showsPagination={false}
                style={styles.swiper}
                loop={false}
                onMomentumScrollEnd = {this._onMomentumScrollEnd}
                height={this._updateBoundaries()}
                >
                    { componentList }
            </Swiper>
        );

    }

render() {
        return (
            <View>
                <Header />
                { this.props.estimateExpenses ? this._renderWithExpenses() : this._renderWithoutExpenses() }
            </View>
        );
    }

... then for each section, I had to deal with different components, so I returned an array of components for this._renderSectionOne() etc. - this prevented me from repeating component code where I could with the limitations.

As an example:

_renderSectionOne() {
        return (
            [<ComponentOne key="0" />,
                          <ComponentTwo key="1" />]
        );
    }

_renderSectionTwo() {
        return (
            [<AnotherComponentOne key="0" />,
                          <ComponentTwo key="1" />]
        );
    }

The important and slightly annoying thing is that I had to rewrite the <Swiper /> components in both the _renderWithExpenses() and _renderWithoutExpenses(), but this was as flexible as I got which managed to work.

All these methods fell within the one component class. It's also important that you combine all the dynamic section arrays into one array before using map() or your Swiper will be retarded af.

Hopefully this makes sense and helps out, fam.

applechips commented 7 years ago

@okeeffed do you have a link to your github with that? i'm still having some trouble understanding it. yoinks!

okeeffed commented 7 years ago

@applechips unfortunately that was part of a production app! But maybe I can try to shorten and clarify a little more?

What this example does is it will go off a Boolean prop that is provided (passed down as a prop or using the combined reducer if you are using redux like I was) - if you're just using state then swap out the prop Boolean for the state Boolean, and depending on the state given you will run a function where in this example if false will call renderTwoSections which in turn will call sectionOne and sectionThree and then using the spread operator will create a brand new array, then create the list by mapping the array and then in the return function you will return a full component with the { componentList } being the mapped out constant we declared about.

If the boolean is true then it will call renderThreeSections which literally will do the same thing, however it will use the spread operator to create an combined array of ALL the arrays returned from sectionOne(), sectionTwo() and sectionThree() and run the same process.

If you needed more flexibility you could turn the ternary operator from the render() method into a switch or something... The only issue I remember having is that when you swap out the components, you won't notice it on a certain slide but the actual refs.swiper.index (it was something like that) will reset to 0 so I just did something to recall what the current index I was up to was so that if you manually change index there wouldn't be an issue.

The REALLY important thing is that those functions sectionOne, sectionTwo, sectionThree return ARRAYS of the components that you wish to render and not just the component built itself. You need to get back arrays, spread operator those guys and then map them out.

Could be a better process but hell I haven't seen anyone else with a solution, just a bunch of "nah can't be done". But you know what? It can be. Yiewwww.

Hopefully this makes more sense then that random financial babble I had up above.

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import {
  Text,
  View
} from 'react-native';
import Swiper from 'react-native-swiper';
import {Actions} from 'react-native-router-flux';

class DynamicSwiperExampleView extends Component {
    sectionOne() {
        return (
            [<View>
                <Text>First element of the Section One Array</Text>
            </View>,
            <View>
                <Text>Second element of the Section One Array</Text>
            </View>]
        );
    }

    sectionTwo() {
        return (
            [<View>
                <Text>First element of the Section Two Array</Text>
            </View>,
            <View>
                <Text>Second element of the Section Two Array</Text>
            </View>]
        );
    }

    sectionThree() {
        return (
            [<View>
                <Text>First element of the Section Three Array</Text>
            </View>,
            <View>
                <Text>Second element of the Section Three Array</Text>
            </View>]
        );
    }

    renderTwoSections() {
        const sectionOneArray = this.sectionOne();
        const sectionThreeArray = this.sectionThree();

        const componentReturn = [...sectionOneArray, ...sectionThreeArray];
        const componentList = componentReturn.map((item) => item);

        return (
            <Swiper 
                ref='swiper'
                showsPagination={false}
                style={styles.swiper}
                loop={false}
                onMomentumScrollEnd = {() => this._onMomentumScrollEnd()}
                height={this._updateBoundaries()}
                >
                    { componentList }
            </Swiper>
        );
    }

    renderThreeSections() {
        const sectionOneArray = this.sectionOne();
        const sectionTwoArray = this.sectionTwo();
        const sectionThreeArray = this.sectionThree();

        const componentReturn = [...sectionOneArray, ...sectionTwoArray, ...sectionThreeArray];
        const componentList = componentReturn.map((item) => item);

        return (
            <Swiper 
                ref='swiper'
                showsPagination={false}
                style={styles.swiper}
                loop={false}
                onMomentumScrollEnd = {() => this._onMomentumScrollEnd()}
                height={this._updateBoundaries()}
                >
                    { componentList }
            </Swiper>
        );

    }

    render() {
        return (
            <View>
                { this.props.showAllThreeSections ?  this.renderTwoSections() : this.renderThreeSections() }
            </View>
        );
    }
}

export default DynamicSwiperExampleView;

In the end, you call renderTwoSections() you will get back

<Swiper 
    ref='swiper'
    showsPagination={false}
    style={styles.swiper}
    loop={false}
    onMomentumScrollEnd = {() => this._onMomentumScrollEnd()}
    height={this._updateBoundaries()}
    >
        <View>
            <Text>First element of the Section One Array</Text>
        </View>
        <View>
            <Text>Second element of the Section One Array</Text>
        </View>
        <View>
            <Text>First element of the Section Three Array</Text>
        </View>
        <View>
            <Text>Second element of the Section Three Array</Text>
        </View>
</Swiper>

That's too much typing and I'm at work so hopefully you can figure out what happens if you call renderThreeSections() with that bool etc.

Also, I cut out a lot... so there are no lifecycle methods etc but this should give you the gist of it

IPrototypeI commented 7 years ago

you can even use an array and then add it to the render method:

render() {
    var example = [];
    example[0] = <Text>Second element of the Section Three Array</Text>;
    return (
        --- some code
        {example}
     );
}
okeeffed commented 7 years ago

So I actually just had to come back here months later because I had to push the app for Android as well, and it turns out my hack from above didn't work for Android. Had to go digging again for another hack 😰

The native Android view in itself isn't inherently dynamic and I didn't come from a Android background. Although the view <ViewPagerAndroid> is based on in Android isn't itself dynamic, the React Native one can be.

Set a key that tells the app to re-render. I altered the source code to add key={pages.length}

return (
      <ViewPagerAndroid ref='scrollView'
        {...this.props}
        initialPage={this.props.loop ? this.state.index + 1 : this.state.index}
        onPageSelected={this.onScrollEnd}
        style={{flex: 1}} key={pages.length}>
        {pages}
      </ViewPagerAndroid>
    )

Literally only difference is that the pages being passed as children are now also being used to set a key. The differing key is important.

Now the one downside is that when the component re-renders, the initial index will be the called upon again, so if this isn't set then you will find yourself back at index 0. That also means that local state of the component will reset unless you are altering state of a higher order component. In my case, I'm using redux, so it became simply a case of having a Redux pageNumber state being held by the reducer.

Since my swiper had both buttons to swipe and natural swiping, I had to update my functions to in turn update the Redux state of pageNumber to increase or decrease. With the buttons, it's simple. If that button is shifting the swiper using this.refs.swiper.scrollBy() then just update the HOC state using the current state +/- the scrollBy argument. If you are using onMomentumScrollEnd from the swiper properties, then just use a basic if/else for this.refs.swiper.state.index > pageNumber (or <... whatever) and update the pageNumber.

Then when you are re-rendering the Swiper component, you can pass down that pageNumber prop as an argument to the swiper property index. This will render the replacement, dynamic swiper at the correct position instead of going back to 0.

https://github.com/facebook/react-native/issues/4775 - this was useful for pulling me in the right direction. If you don't alter the key, the re-render will not work correctly.

RichardLindhout commented 7 years ago

This works 100% on iOS, not tested on Android yet.

import React, { Component } from 'react'
import { View, Text, Dimensions, VirtualizedList } from 'react-native'

const { width, height } = Dimensions.get('window')
const startAtIndex = 5000
const stupidList = new Array(startAtIndex * 2)

class InfiniteViewPager extends Component {
  //only use if you want current page
  _onScrollEnd(e) {
    // const contentOffset = e.nativeEvent.contentOffset
    // const viewSize = e.nativeEvent.layoutMeasurement
    // // Divide the horizontal offset by the width of the view to see which page is visible
    // const pageNum = Math.floor(contentOffset.x / viewSize.width)
  }
  _renderPage(info) {
    const { index } = info

    return (
      <View style={{ width, height }}>
        <Text>
          {' '}{`index:${index}`}{' '}
        </Text>
      </View>
    )
  }
  render() {
    return (
      <VirtualizedList
        horizontal
        pagingEnabled
        initialNumToRender={3}
        getItemCount={data => data.length}
        data={stupidList}
        initialScrollIndex={startAtIndex}
        keyExtractor={(item, index) => index}
        getItemLayout={(data, index) => ({
          length: width,
          offset: width * index,
          index,
        })}
        maxToRenderPerBatch={1}
        windowSize={1}
        getItem={(data, index) => ({ index })}
        renderItem={this._renderPage}
        onMomentumScrollEnd={this._onScrollEnd}
      />
    )
  }
}
VanceLongwill commented 7 years ago

I had this problem too. I solved it by spreading the generated array inside a new single purpose array inside the render(). Both the views use keys from react-native-uuid. I'm not sure if this will work in all circumstances but it worked for me so I wanted to share it.

render(){
    return(
        {
        [
          // previously defined array which will get updated at some point
        ...generatedArray,
        // static slide 
        <View style={styles.slide} key={uuid.v4()}>
          <Button
            title="end quiz"
            action={() => console.log}
          />
        </View>,
        ]
        }
    )
}
kevindavee commented 6 years ago

Hi guys. I also had this problem. But I managed to solve this problem by implementing this method. Hope you all can try and solve your problem


        return(
            <View style={styles.welcomeScreenContainer} key={index}>
                <View style={{flex: 2}}>
                    <View style={[styles.centerOfTheGrid,styles.gridView]}>
                        <Text style={[styles.slideText, styles.titleText]}>{slide.title}</Text>
                        <Text style={[styles.slideText, styles.descriptionText]}>{slide.content}</Text>
                    </View>
                </View>
                <View style={{flex: 4}}>
                    <View style={[styles.topOfTheGrid,styles.gridView]}>
                        <Image source={{ uri: slide.image }} style={{ width: IMAGE_WIDTH, height: IMAGE_HEIGHT }}/>
                    </View>
                </View>
                <View style={{flex: 2}}>
                    <View style={[styles.topOfTheGrid, styles.gridView, styles.button]}>
                        <Button primary onPress={this.handlePressSkip}>
                            <Text>Browse Menu</Text>
                        </Button>
                    </View>
                </View>
            </View>
        )
    }
    renderSlides() {
        const {slides} = this.props.welcomeScreenState
        let mutableSlides = []
        for (let slide of slides) {
            mutableSlides.push({...slide})
        }
        mutableSlides.push({title: 'login'})
        return(
            <Swiper showsButtons={true} loop={false}>
                {mutableSlides.map((slide, index) => {
                    if (slide.title === 'login') {
                        return (
                            <LoginScreen/>
                        )
                    } else {
                        return this.renderSlide(slide, index)
                    }
                })}
            </Swiper>   
        )
    }

    render() {
        return(
            <View style={{flex:1}}>    
                 {this.renderSlides()}
            </View>
        )
    }```
schouffy commented 6 years ago

Hey guys, I have tried everything suggested in here. Nothing worked :( Static views inside the Swiper work as expected, dynamic views will be stacked in the same view. Has anyone discovered something else ?

dhcmega commented 6 years ago

@schouffy it's working for me. I have an array of buttons, I slice them in groups of 6 and then generate de pages. My problem is that the blue dot doesn't move.

This is what I do:

    return (
      <View style={{
        flex: 1,
      }}
      >
        <ImageBackground
          style={{
            flex: 1,
            width: null,
            height: null,
          }}
          source={{ uri }}
          resizeMode="cover"
        >
          {this.props.condominios.menu === null && (<Spinner />)}
          <Image
            style={{ height: 120, margin: 30 }}
            source={{ uri: `${Config.home_bg}` }}
            resizeMode="contain"
          />
          <Swiper
            loop={false}
          >
            {slicedMenu.map((group, group_key) => (
              <View
                key={group_key}
                style={{
                  flex: 1,
                  justifyContent: 'center',
                  alignItems: 'center',
                  // backgroundColor: '#befb1a',
                }}
              >
                <View style={{ position: 'absolute', bottom: 20 }}>
                  <View style={{
                    flexDirection: 'row',
                    // justifyContent: 'space-around',
                    alignItems: 'flex-start',
                    flexWrap: 'wrap',
                  }}
                  >
                    {group.map((item, key) => (
                      <TouchableOpacity
                        key={key}
                        onPress={() => this.actionMenu(item)}
                        underlayColor="#FFFFFF"
                        style={{
                          width: '33%',
                          height: 100,
                          marginBottom: 15,
                          alignItems: 'center',
                        }}
                      >
                        <View style={{
                          justifyContent: 'center',
                          alignItems: 'center',
                          borderRadius: 40,
                          width: 60,
                          height: 60,
                          backgroundColor: 'rgba(44,44,44,0.9)',
                        }}
                        >
                          <MaterialCommunityIcons
                            name={item.icon}
                            size={30}
                            style={{
                              color: '#FFFFFF',
                            }}
                          />
                        </View>
                        <Text style={{
                          color: 'white',
                          fontSize: 15,
                          // marginBottom: 20,
                          textAlign: 'center',
                          marginTop: 3,
                          textShadowColor: '#000000',
                          textShadowOffset: { width: 1, height: 1 },
                          textShadowRadius: 5,
                        }}
                        >
                          {item.display_name}
                        </Text>
                      </TouchableOpacity>
                    ))
                    }
                  </View>
                </View>
              </View>
            ))
            }
          </Swiper>
        </ImageBackground>
      </View>
    );
schouffy commented 6 years ago

This is driving me nuts, i have basically the same thing and they stack in the same "page" of the swiper , each view being "100/(number of views) %" height. If instead of generating the views in a array.map, i declare the exact same views statically, it works as expected. Can you confirm it works on Android ?

dhcmega commented 6 years ago

My code works on iphone and android, BUT it doesn't generate the dots per page, so pages swipe but there is always one dot. I would think that this is because the dot/dots are generated before my ajax responds with the elements to display, if I remove the bigger map and replicate the code twice, it all works ok. I mean, 2 pages and dot working ok.

EDIT: I added a spinner while loading and now the dots show up as they should.

schouffy commented 6 years ago

I fixed my issue. I still don't get it but i'm new to react so maybe someone can help understand ? And maybe this can help someone that has the same issue too.

Near the end of the file, in the render function for the Swiper, there is :

`if (total > 1) { // Re-design a loop model for avoid img flickering pages = Object.keys(children) if (loop) { pages.unshift(total - 1 + '') pages.push('0') }

  pages = pages.map((page, i) => {
    if (loadMinimal) {
      if (i >= (index + loopVal - loadMinimalSize) &&
        i <= (index + loopVal + loadMinimalSize)) {
        return <View style={pageStyle} key={i}>{children[page]}</View>
      } else {
        return (
          <View style={pageStyleLoading} key={i}>
            {loadMinimalLoader ? loadMinimalLoader : <ActivityIndicator />}
          </View>
        )
      }
    } else {
      return <View style={pageStyle} key={i}>{children[page]}</View>
    }
  })
} else {
  pages = <View style={pageStyle} key={0}>{children}</View>
}`

basically this creates the Views elements that will be displayed inside the Swiper. In my case, "children" is a dynamic array of views generated from json data populated in my component constructor. When i display the Swiper, total is supposed to be 4 (number of items in my json), but it's actually 0 so it processes this pages = <View style={pageStyle} key={0}>{children}</View> Which stacks my 4 views in a single one.

To correct my issue i changed this line to : pages = children; // was <View style={pageStyle} key={0}>{children}</View>

So my items are dislayed "as they are", not included in a single item.

I hope someone can clarify why total == 0, in the meantime i can work. Cheers.

infogod commented 6 years ago

My issue is about dynamic views. I have a component called Table inside a Swiper. According to some events on my component the system will add more tables to Swiper but when it happens all the components added are recreated but I need to add news tables to the swiper keeping the state of the tables already added.

example code:

 render() {
         return (
             <Swiper loop={false}  showsPagination={false} showsButtons={false}>
                 {this.tables.map((item,key)=>{
                     return (<View key={item.id} style={{flex: 1}}><Table
                                    game_id = {item.game_id}
                                    name={item.name}
                                    id={item.id}
                                    button={item.button}
                                    lateEnter={item.lateEnter}
                                    mynavigation={item.mynavigation}/></View>)
                 })}
             </Swiper>
         );
     }
 };
thisisirfan commented 4 years ago

I hope this example will help: https://github.com/leecade/react-native-swiper/blob/master/examples/components/Dynamic/index.js