React Native Modal İçinde ScrollView Neden Çalışmaz?
React Native projenizde bir paylaşım önizleme modalı (Share Preview Modal) geliştirdiniz ancak ScrollView bileşeni bir türlü kaydırılmıyor mu? Bu makalede, bu kilitlenmenin altında yatan temel nedenleri ve adım adım çözüm yöntemlerini ayrıntılı şekilde inceliyoruz.
1. React Native Modal ve ScrollView Odaklı Ekran Karmaşası
Mobil uygulama geliştirme sürecinde kullanıcı deneyimini (UX) üst seviyede tutmak son derece kritiktir. Özellikle bir içeriği, görseli veya makaleyi dış uygulamalarla paylaşmadan önce kullanıcıya sunulan Share Preview Modal (paylaşım önizleme penceresi) ekranları, modern mobil mimarilerin vazgeçilmez bir parçasıdır. Ancak geliştiriciler, bu alt pencerelerin içerisine yüksek miktarda içerik yerleştirmek istediklerinde sıklıkla kaydırma (scrolling) sorunlarıyla karşılaşırlar.
React Native ekosisteminde ScrollView veya FlatList gibi kaydırılabilir bileşenler, yerel (native) platformların dokunma duyarlılığı mimarisini kullanır. Bir modal içerisine yerleştirilen kaydırma alanı yanıt vermiyorsa, problem genellikle Javascript katmanından ziyade Native UI (yerel kullanıcı arayüzü) katmanındaki boyut hesaplamalarından veya dokunma olayı (touch responder) çakışmalarından kaynaklanır. Bunun sonucunda kullanıcı parmağını ekranda sürüklese dahi içerik sabit kalır. Bu durum mobil uygulamanın profesyonelliğine gölge düşürür.
Bu makale boyunca, paylaşım önizleme modallarında yaşanan kaydırma problemlerini kökten çözmek için gerekli olan temel layout (düzen) kurallarını, jest yönetim sistemlerini ve platform bazlı teknik nüansları ele alacağız. Üstelik karmaşık akademik terimler yerine, gerçek hayat senaryolarıyla ve uygulanabilir kod bloklarıyla konuyu sıfırdan ileri seviyeye kadar inşa edeceğiz.
2. Layout (Düzen) Esasları: Flexbox Neden ScrollView Bileşenini Kilitler?
React Native tarafında en sık yapılan hatalardan biri, ScrollView bileşeninin çalışma prensibini standart bir View bileşeni ile aynı varsaymaktır. Bir View bileşeni çocuklarının (children) boyutuna göre genişleyebilirken, ScrollView bileşeninin kaydırma işlevini gerçekleştirebilmesi için kesinlikle sınırlandırılmış bir yüksekliğe (bounded height) ihtiyacı vardır. Eğer ebeveyn (parent) kapsayıcı kendi yüksekliğini belirleyemiyorsa, ScrollView içerik yüksekliğini hesaplayamaz ve kaydırma mekanizmasını tamamen kapatır.
Özellikle Modal bileşeni varsayılan olarak ekranın tamamını kaplayan ayrı bir yerel pencere (native window) oluşturur. Bu pencere içerisine eklenen flex mimarisi doğru yapılandırılmazsa yükseklik zinciri kırılır. Sonuç olarak ScrollView sonsuz bir yükseklikte olduğunu varsayar ve kaydırma çubuğunu pasif hale getirir.
Flex: 1 Yapısını Doğru Anlamak ve Uygulamak
Bir ScrollView elemanının kapsayıcı modal içinde sorunsuz çalışabilmesi için iki temel prop (özellik) seviyesinde düzenleme yapılması gerekir. Bunlardan ilki bileşenin kendi dış sınırını belirleyen style prop’u, ikincisi ise içerideki elemanların dizilimini kontrol eden contentContainerStyle prop’udur.
Bu iki kavram arasındaki farkları anlamak sorunun çözümü için hayati önem taşır. Aşağıdaki tabloda bu iki prop üzerindeki layout davranışları karşılaştırılmıştır:
| Özellik (Prop) | Etki Alanı | Kritik Stil Tanımı | Hata Belirtisi |
|---|---|---|---|
style |
ScrollView’ın dış kapsayıcı boyutunu belirler. | flex: 1 |
İçerik tamamen görünmez olur veya ekrandan taşar. |
contentContainerStyle |
İçerideki elemanların toplam alanını kapsar. | flexGrow: 1 |
Ekran dolar ancak kaydırma hareketi gerçekleşmez. |
Görüldüğü üzere, contentContainerStyle içerisinde flex: 1 kullanmak sıklıkla yapılan bir hatadır. Eğer flex: 1 verirseniz, kaydırma alanı ekran boyutuna zorla eşitlenir ve kaydırma esnekliği kaybolur. Bunun yerine flexGrow: 1 kullanmak, içeriğin minimum ekran kadar genişlemesini ama ihtiyaç halinde uzayarak kaydırılabilir olmasını sağlar.
3. Dokunma Olayı (Touch Event) Çakışmaları ve Gesture Handler Rolü
Paylaşım önizleme modallarında kaydırma yapılamamasının bir diğer majör sebebi dokunma olaylarının (touch events) yanlış bileşen tarafından yakalanmasıdır (interception). React Native, dokunma hareketlerini yönetmek için bir Dokunma Yanıtlayıcı Sistemi (Gesture Responder System) kullanır. Bir kullanıcı ekrana dokunduğunda, bileşen ağacındaki en üstteki veya en uygun olan eleman bu dokunmayı sahiplenir.
Özellikle alt taraftan açılan modal (Bottom Sheet) tasarımlarında, modala sürükleyerek kapatma (drag to dismiss) özelliği eklenmişse durum karmaşıklaşır. Modalın kendi pan hareketi (PanResponder) ile ScrollView bileşeninin dikey kaydırma hareketi birbiriyle rekabete girer. Eğer modalın jest yakalayıcısı daha baskınsa, kullanıcı parmağını yukarı sürüklediğinde içerik kaymak yerine modalın kendisi hareket eder veya jest tamamen yutulur.
PanResponder ve Dokunma Yakalayıcı Metotlar
Android ve iOS işletim sistemleri dokunma önceliklerini farklı şekillerde yönetir. Örneğin Android üzerinde iç içe geçmiş kaydırılabilir alanlar varsayılan olarak kilitlenebilir. Bu durumu aşmak için React Native bileşeninde nestedScrollEnabled özelliğinin aktif edilmesi şarttır.
Bununla birlikte, modern uygulamalarda standart React Native jest mekanizması yerine react-native-gesture-handler kütüphanesi tercih edilir. Bu kütüphane, JavaScript katmanındaki gecikmeleri (thread lag) atlayarak doğrudan yerel seviyede jest takibi yapar. Dolayısıyla paylaşım modalı içerisinde standart bir ScrollView yerine bu kütüphanenin sunduğu özel kaydırma elemanlarını kullanmak çakışmaları engeller.
4. Paylaşım Önizleme (Share Preview) Modallarındaki Özel Nedenler
Paylaşım önizleme modalları yapıları gereği standart bir uyarı (alert) modalından farklıdır. Genellikle içeriklerinde dinamik olarak yüklenen metinler, URL önizleme kartları, görsel galerileri ve hedef platform seçici butonlar barındırırlar. Bu karmaşık yapı, render (ekrana çizdirme) süreçlerinde gecikmelere ve boyut hesaplama hatalarına yol açar.
Özellikle dış bir uygulamadan Share Extension (paylaşım uzantısı) kanalıyla veri alındığında, verinin modala ulaşması asenkron bir süreçtir. Veri geldiğinde modal boyutu aniden değişebilir. Eğer ScrollView yüksekliğini veri yüklenmeden önce hesapladıysa, içerik gelse dahi kaydırma sınırları güncellenmeyebilir.
Bir diğer yaygın senaryo ise modalın arkasında kalan karartı alanına (backdrop) tıklayınca modalın kapanması için eklenen TouchableOpacity veya Pressable bileşenleridir. Tüm modal içeriği bu kapatma alanının içine sarmalandıysa, dokunma olayları ScrollView bileşenine ulaşamadan dışarıdaki tıklama elemanı tarafından yutulmaktadır.
5. Adım Adım Çözüm Rehberi: Kod Örnekleri ile Düzeltme
Sorunu teorik olarak anladıktan sonra, şimdi uygulamalı çözüm aşamasına geçebiliriz. Aşağıda kilitlenen bir paylaşım önizleme modalı kodu ile bu sorunun düzeltilmiş, en iyi pratikleri (best practices) içeren halini bulabilirsiniz.
Senaryo 1: Hatalı Kod Yapısı ve Tespit Edilen Yanlışlar
Aşağıdaki kod örneğinde, geliştiricilerin sıklıkla düştüğü layout ve kapsayıcı hataları yer almaktadır. Bu kod parçası Android ve iOS üzerinde kaydırma yapmayacaktır.
import React from 'react';
import { Modal, View, Text, ScrollView, TouchableOpacity } from 'react-native';
const BadShareModal = ({ visible, onClose, shareData }) => {
return (
<Modal visible={visible} animationType="slide" transparent={true}>
<TouchableOpacity style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} onPress={onClose}>
{/* HATA 1: TouchableOpacity tüm alanı kaplayıp dokunmaları yutuyor */}
<View style={{ backgroundColor: 'white', marginTop: 100 }}>
{/* HATA 2: Yükseklik sınırı yok ve flex tanımlanmamış */}
<Text style={{ fontSize: 20, padding: 15 }}>Paylaşım Önizleme</Text>
<ScrollView style={{ height: 'auto' }} contentContainerStyle={{ flex: 1 }}>
{/* HATA 3: contentContainerStyle içinde flex: 1 kullanımı kaydırmayı kilitler */}
<Text>{shareData.longText}</Text>
{/* Dinamik gelen uzun içerikler */}
</ScrollView>
</View>
</TouchableOpacity>
</Modal>
);
};
export default BadShareModal;
Senaryo 2: Çalışan ve İdeal Kod Mimarısı
Hatalı yapıdaki problemleri çözmek için: Dokunma yutma sorununu engellemek amacıyla karartı alanını ayrıştırıyoruz, ScrollView bileşenine flex: 1 veriyoruz ve Android için nestedScrollEnabled ekliyoruz. Ayrıca contentContainerStyle içerisinde flexGrow: 1 kullanıyoruz.
import React from 'react';
import { Modal, View, Text, ScrollView, Pressable, StyleSheet } from 'react-native';
const GoodShareModal = ({ visible, onClose, shareData }) => {
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
>
<View style={styles.modalOverlay}>
{/* Arka plan kapatma alanı bağımsız bir Pressable olarak konumlandırıldı */}
<Pressable style={styles.backdrop} onPress={onClose} />
<View style={styles.modalContainer}>
<View style={styles.header}>
<Text style={styles.headerText}>Paylaşım Önizleme</Text>
</View>
<ScrollView
style={styles.scrollArea}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled={true}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={true}
>
<Text style={styles.bodyText}>{shareData?.longText}</Text>
{/* Ek önizleme bileşenleri */}
</ScrollView>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
},
modalContainer: {
maxHeight: '80%', // Modalın ekrandan taşmasını engeller ve sınır çizer
minHeight: '30%',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
overflow: 'hidden',
},
header: {
padding: 16,
},
headerText: {
fontSize: 18,
fontWeight: 'bold',
},
scrollArea: {
flex: 1, // ScrollView'in kalan tüm alanı kaplamasını sağlar
},
scrollContent: {
flexGrow: 1, // İçeriğin esnek şekilde büyümesine izin verir
padding: 16,
},
bodyText: {
fontSize: 14,
lineHeight: 20,
},
});
export default GoodShareModal;
6. İleri Düzey İpuçları: Android ve iOS Tarafındaki Mimari Farklar
React Native uygulamalarında her iki platform da kendi yerel grafik işleme modüllerini çalıştırır. Dolayısıyla yazılan aynı JavaScript kodu, iOS ve Android tarafında farklı sonuçlar doğurabilir. Paylaşım modallarındaki kaydırma krizlerinde platform bazlı şu ayrıntılara dikkat etmek gerekir:
- iOS Native Presentation Style: iOS ortamında
Modalbileşenine verilenpresentationStyle="pageSheet"özelliği, işletim sisteminin kendi alt sayfa jestlerini devreye sokar. Bu durum içtekiScrollViewjestleri ile çatışabilir. Eğer sorun yaşarsanız bu değerioverFullScreenolarak değiştirmek kontrolü tekrar React Native tarafına verir. - Android Hardware Acceleration ve Nested Scrolling: Android işletim sisteminde iç içe geçmiş kaydırma alanları yerel düzeyde kapalı gelebilir.
nestedScrollEnabled={true}kullanmak sadeceScrollViewiçin değil, eğer modal içindeFlatListveyaSectionListkullanılıyorsa onlar için de zorunludur. - Fabric Renderer (Yeni Mimari): React Native’in Yeni Mimarısı (New Architecture – Fabric) ile birlikte jest yönetiminde performans artışı sağlandı. Ancak Fabric mimarisinde boyut hesaplamaları senkron yapıldığı için
maxHeightdeğerinin modal kapsayıcısına kesin olarak verilmiş olması eskisine göre daha da kritik hale gelmiştir.
7. Gerçek Dünya Senaryosu: E-Ticaret Uygulamasında Ürün Paylaşım Modalı
Konunun pekişmesi için Türkiye pazarında faaliyet gösteren büyük bir e-ticaret platformunun karşılaştığı gerçek bir mühendislik problemini inceleyelim.
Söz konusu şirket, kullanıcıların bir sepeti veya ürün listesini WhatsApp ve Instagram üzerinden paylaşabilmesi için özel bir “Paylaşım Önizleme Modalı” geliştirdi. Bu modalın içinde hem seçili ürün görsellerinin olduğu yatay bir ScrollView hem de ürün detaylarının, indirim kuponlarının yazıldığı dikey bir ScrollView yer alıyordu.
Canlıya çıkış sonrasında özellikle Android kullanıcılarından “Paylaşım ekranında aşağıya inemiyoruz, ekran donuyor” şeklinde yoğun geri bildirimler alındı. Yapılan teknik analizde şu problemler tespit edildi:
- Yatay ve dikey kaydırma alanları iç içe geçmişti ancak Android için
nestedScrollEnabledtanımı unutulmuştu. - Modal alt taraftan yukarı doğru açılan bir sürüklenebilir kart (Bottom Sheet) olarak tasarlanmıştı. Kartın yukarı sürükleme jesti, dikey
ScrollViewhareketini tamamen engelliyordu. - Görseller internetten asenkron olarak yüklendiği için görsel geldikten sonra layout yeniden hesaplanmıyor, yükseklik ilk duruma göre kilitleniyordu.
Ekip ilk adım olarak standart React Native Modal yapısı yerine @gorhom/bottom-sheet kütüphanesine geçiş yaptı. Bu kütüphanenin sunduğu BottomSheetScrollView bileşenini devreye aldılar. Bu özel bileşen, modalın sürüklenme jesti ile içeriğin kaydırma jestini otomatik olarak koordine eder (gesture delegation). Ayrıca görseller yüklenirken onLayout tetikleyicisi ile boyut güncellemeleri garanti altına alındı. Sonuç olarak kullanıcıların yaşadığı donma ve kilitlenme problemleri tamamen çözüldü.
8. Özet ve Sonuç
React Native projelerinde paylaşım önizleme modalları (Share Preview Modals) içerisinde yaşanan ScrollView kaymama sorunu, sanılanın aksine karmaşık bir yazılım hatası değil, temel layout ve jest yönetimi eksikliğidir. Bir modal içinde kaydırma alanını sorunsuz çalıştırmak için şu kurallar daima hatırlanmalıdır:
Dış kapsayıcı kesinlikle sınırlandırılmış bir yüksekliğe (maxHeight veya flex: 1) sahip olmalıdır. ScrollView bileşeninin kendisine flex: 1 verilmeli, iç içeriği yöneten contentContainerStyle özelliğinde ise kesinlikle flex: 1 yerine flexGrow: 1 tercih edilmelidir. Tıklama ve kapatma olaylarını yakalayan katmanlar kaydırma alanının arkasına yerleştirilmeli, Android cihazlar için nestedScrollEnabled ibaresi ihmal edilmemelidir.
Bu standartları uygulayarak uygulamanızdaki modalların hem iOS hem de Android platformunda akıcı, esnek ve yerel (native) kalitede çalışmasını sağlayabilirsiniz.
9. Sıkça Sorulan Sorular
1. ScrollView içerisindeki style ile contentContainerStyle arasındaki fark nedir?
style prop’u ScrollView bileşeninin ekran üzerindeki dış boyutunu ve sınırlarını (frame) belirler. contentContainerStyle ise ScrollView içindeki elemanları sarıp sarmalayan iç alanı yönetir. Kaydırma yapılabilmesi için dış alanın sabit veya sınırlı, iç alanın ise esnek (flexGrow: 1) olması gerekir.
2. Android cihazlarda iç içe kaydırma sorunu neden yaşanır ve nasıl çözülür?
Android işletim sistemi dokunma çakışmalarını önlemek için varsayılan olarak iç içe geçmiş kaydırma alanlarında içteki elemanın jestlerini kısıtlayabilir. Bu durumu çözmek için ScrollView bileşenine nestedScrollEnabled={true} özelliğini eklemek yeterlidir.
3. Modal karartma alanına (backdrop) eklediğim tıklama özelliği kaydırmayı engeller mi?
Evet, eğer tüm modal içeriğini kapsayan bir TouchableOpacity veya Pressable kullanırsanız, bu eleman tüm dokunma olaylarını kendi üzerine alabilir ve ScrollView bileşenine dokunma ulaşmasını engeller. Çözüm olarak karartma alanını modal içeriğinden bağımsız mutlak konumlandırılmış (position: 'absolute') ayrı bir eleman olarak yazmalısınız.
4. React Native standart ScrollView yerine Gesture Handler kullanmak şart mıdır?
Basit modallarda standart React Native ScrollView yeterlidir. Ancak sürüklenerek kapatılan karmaşık alt modallar (Bottom Sheet) veya iç içe çok sayıda jest içeren yapılarda react-native-gesture-handler kütüphanesinin ScrollView bileşenini kullanmak jest çakışmalarını tamamen ortadan kaldırır.
#ReactNative #MobileDevelopment #Javascript #SoftwareArchitecture #MobileUX