0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

友人たちと作ってるサービスのWebアプリで会員登録ページを作成している際,意外な問題に直面しました

それは
PWAで動くdataPicker見つけられない!

将来的なiOS/Androidへの横展開を目指してExpo(React Native) WebでPWAチックに作っているのですがiOS,android上で動くdatePickerは
https://docs.expo.dev/versions/latest/sdk/date-time-picker/

https://github.com/react-native-datetimepicker/datetimepicker?tab=readme-ov-file#expo-users-notice
のようなものはあるけれどPWAで動くdatePickerがなかなか見つけられない!

↓こういうの
image.png

ここまでやってる人はもうiOS/Androidアプリまで作れてるよね?って話なのだろうか...
かつては react-ios-wheel-picker というパッケージがあったらしいのだが 404 not foundになっている

1日探しても見つからないので,しょうがない,作るかと言うことになりました

作成したもののスクショ

simulator_screenshot_CEA0A097-F6FC-4C01-90C2-3A1B7CAD423D.png

とりあえずChatGPT君に投げた

優秀だからコードの作成までやってくれて問題なく動いたのでコードの内容を見てみる

作ってくれたコード

CustomWheelPicker.tsx
import React, { useEffect, useRef, useState } from 'react';
import {
  View,
  Text,
  ScrollView,
  StyleSheet,
  TouchableOpacity,
  Dimensions,
  Modal,
  NativeSyntheticEvent,
  NativeScrollEvent,
} from 'react-native';

type Props = {
  dataset: string[];
  initialValue?: string;
  onConfirm: (value: string) => void;
  onCancel?: () => void;
};

const ITEM_HEIGHT = 48;

const CustomWheelPicker = ({
  dataset,
  initialValue,
  onConfirm,
  onCancel,
}: Props) => {
  const [selectedIndex, setSelectedIndex] = useState(0);
  const scrollRef = useRef<ScrollView>(null);

  useEffect(() => {
    const index = dataset.findIndex((d) => d === initialValue);
    if (index >= 0) {
      setSelectedIndex(index);
      scrollRef.current?.scrollTo({ y: index * ITEM_HEIGHT, animated: false });
    }
  }, [initialValue]);

  const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
    const y = e.nativeEvent.contentOffset.y;
    const index = Math.round(y / ITEM_HEIGHT);
    setSelectedIndex(index);
  };

  const confirmValue = () => {
    onConfirm(dataset[selectedIndex]);
  };

  return (
    <Modal transparent animationType="slide">
      <View style={styles.overlay}>
        <View style={styles.pickerContainer}>
          {/* ヘッダー */}
          <View style={styles.header}>
            <TouchableOpacity onPress={onCancel}>
              <Text style={styles.cancelText}>キャンセル</Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={confirmValue}>
              <Text style={styles.doneText}>完了</Text>
            </TouchableOpacity>
          </View>

          {/* ホイール本体 */}
          <View style={styles.wheelWrapper}>
            <ScrollView
              ref={scrollRef}
              snapToInterval={ITEM_HEIGHT}
              decelerationRate="fast"
              showsVerticalScrollIndicator={false}
              onMomentumScrollEnd={handleScrollEnd}
              contentContainerStyle={{ paddingVertical: ITEM_HEIGHT * 2 }}
            >
              {dataset.map((item, index) => (
                <View key={index} style={styles.item}>
                  <Text
                    style={[
                      styles.itemText,
                      index === selectedIndex && styles.itemTextSelected,
                    ]}
                  >
                    {item}
                  </Text>
                </View>
              ))}
            </ScrollView>
            <View style={styles.highlight} />
          </View>
        </View>
      </View>
    </Modal>
  );
};

const { width } = Dimensions.get('window');

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    justifyContent: 'flex-end',
    backgroundColor: 'rgba(0,0,0,0.4)',
  },
  pickerContainer: {
    backgroundColor: '#fff',
    borderTopLeftRadius: 12,
    borderTopRightRadius: 12,
    paddingBottom: 20,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingHorizontal: 20,
    paddingTop: 12,
    paddingBottom: 8,
  },
  cancelText: {
    fontSize: 16,
    color: '#999',
  },
  doneText: {
    fontSize: 16,
    color: '#007AFF',
    fontWeight: '600',
  },
  wheelWrapper: {
    height: ITEM_HEIGHT * 5,
    width: width,
    position: 'relative',
  },
  item: {
    height: ITEM_HEIGHT,
    justifyContent: 'center',
    alignItems: 'center',
  },
  itemText: {
    fontSize: 20,
    color: '#666',
  },
  itemTextSelected: {
    fontWeight: 'bold',
    color: '#000',
  },
  highlight: {
    position: 'absolute',
    top: ITEM_HEIGHT * 2,
    height: ITEM_HEIGHT,
    left: 0,
    right: 0,
    borderTopWidth: 1,
    borderBottomWidth: 1,
    borderColor: '#ccc',
  },
});

export default CustomWheelPicker;

内容


type Props = {
  dataset: string[];
  initialValue?: string;
  onConfirm: (value: string) => void;
  onCancel?: () => void;
};


const CustomWheelPicker = ({
  dataset,
  initialValue,
  onConfirm,
  onCancel,
}: Props) => {

データセットを引数としているのでいろんな選択肢を持てる

  const [selectedIndex, setSelectedIndex] = useState(0);

selectIndexでdetaset[i]のiを制御

  const scrollRef = useRef<ScrollView>(null);

設定された初期値が画面外にあるのでそこにすぐにアクセスできるようにする

 useEffect(() => {
   const index = dataset.findIndex((d) => d === initialValue);
   if (index >= 0) {
     setSelectedIndex(index);
     scrollRef.current?.scrollTo({ y: index * ITEM_HEIGHT, animated: false });
   }
 }, [initialValue]);

1つの項目を移動するのに何ピクセル必要か

  const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
    const y = e.nativeEvent.contentOffset.y;
    const index = Math.round(y / ITEM_HEIGHT);
    setSelectedIndex(index);
  };

最後の値に行ったら止まるようにする

  const confirmValue = () => {
    onConfirm(dataset[selectedIndex]);
  };

今のIndexの値を確定

  return (
    <Modal transparent animationType="slide">

Modal
https://reactnative.dokyumento.jp/docs/modal
slideは下からにゅっと出てくる,
すぐに出てきて欲しいならnone

      <View style={styles.overlay}>
        <View style={styles.pickerContainer}>
          {/* ヘッダー */}
          <View style={styles.header}>
            <TouchableOpacity onPress={onCancel}>
              <Text style={styles.cancelText}>キャンセル</Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={confirmValue}>
              <Text style={styles.doneText}>完了</Text>
            </TouchableOpacity>
          </View>

ヘッダーにある「キャンセル」「確定」

          {/* ホイール本体 */}
          <View style={styles.wheelWrapper}>
            <ScrollView
              ref={scrollRef}
              snapToInterval={ITEM_HEIGHT}
              decelerationRate="fast"
              showsVerticalScrollIndicator={false}
              onMomentumScrollEnd={handleScrollEnd}
              contentContainerStyle={{ paddingVertical: ITEM_HEIGHT * 2 }}
            >

ref={scrollRef}:初期値の位置に移動
snapToInterval={ITEM_HEIGHT}:各項目ごとで吸着する
decelerationRate="fast":ホイールの減速率
showsVerticalScrollIndicator={false}:スクロールバー非表示
onMomentumScrollEnd={handleScrollEnd}:スクロールが止まった位置で確定
contentContainerStyle={{ paddingVertical: ITEM_HEIGHT * 2 }}
:中央が選択した値になるように余白を作成

              {dataset.map((item, index) => (
                <View key={index} style={styles.item}>
                  <Text
                    style={[
                      styles.itemText,
                      index === selectedIndex && styles.itemTextSelected,
                    ]}
                  >
                    {item}
                  </Text>
                </View>
              ))}

データセットを並べ直す

            </ScrollView>
            <View style={styles.highlight} />
          </View>
        </View>
      </View>
    </Modal>
  );
};

const { width } = Dimensions.get('window');


export default CustomWheelPicker;

色々な場面で使いそうなのにないのは意外だったな

簡単だから,作れってことかな

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?