Dlaczego publikacja aplikacji mobilnej to nie jest „ostatni krok”
Napisałeś aplikację w .NET MAUI, przetestowałeś ją na emulatorze, odpaliłeś na fizycznym urządzeniu — wszystko śmiga. I wtedy przychodzi ten moment, kiedy wielu deweloperów uderza w ścianę: opublikowanie aplikacji w Google Play i App Store to zupełnie osobna dyscyplina. Taka, która potrafi pochłonąć więcej czasu niż napisanie samej aplikacji.
Certyfikaty, provisioning profiles, keystore’y, privacy manifesty, wymagania co do API Level... Szczerze? To jest labirynt. A jeden błąd na dowolnym etapie oznacza odrzucenie i zaczynanie od nowa.
Według danych z 2025 roku ponad 30% pierwszych zgłoszeń do App Store jest odrzucanych z przyczyn technicznych — brakujące ikony, niepoprawne certyfikaty, brak privacy manifestu. Na Google Play jest trochę lepiej, ale błędy z podpisywaniem i wersjonowaniem to nadal codzienność.
W tym przewodniku przeprowadzę Cię przez cały proces publikacji aplikacji .NET MAUI 10 — od przygotowania projektu, przez podpisywanie i budowanie paczek, aż po wrzucenie do sklepów. Skupiam się na podejściu z linii poleceń (dotnet publish), bo jest powtarzalne, daje się zautomatyzować i działa zarówno na Windows, jak i na macOS. Każdy krok zawiera działający kod do zaadaptowania w Twoim projekcie.
Przygotowanie projektu .NET MAUI do publikacji
Zanim zaczniesz budować paczki, jest kilka rzeczy do ogarnięcia w pliku projektu. Niby drobiazgi, ale bez nich sklepy po prostu odrzucą Twoją aplikację.
Identyfikator aplikacji i wersjonowanie
Każda aplikacja w Google Play i App Store musi mieć unikalny identyfikator oraz poprawne numery wersji. W .NET MAUI 10 konfigurujemy to w pliku .csproj:
<PropertyGroup>
<!-- Identyfikator aplikacji — musi być unikalny w sklepie -->
<ApplicationId>com.mojafirma.mojaaplikacja</ApplicationId>
<!-- Wersja wyświetlana użytkownikowi (np. 1.2.0) -->
<ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
<!-- Wersja wewnętrzna — musi rosnąć z każdym uploadem -->
<ApplicationVersion>1</ApplicationVersion>
<!-- Tytuł aplikacji widoczny na urządzeniu -->
<ApplicationTitle>Moja Aplikacja</ApplicationTitle>
</PropertyGroup>
Parę zasad, które warto zapamiętać:
ApplicationVersionto liczba całkowita, która musi rosnąć z każdym uploadem. Sklep odrzuci paczkę z tym samym lub niższym numerem — bez dyskusji.ApplicationDisplayVersionto ciąg znaków widoczny dla użytkownika. Może być dowolny (np.1.2.0-beta), ale Semantic Versioning (MAJOR.MINOR.PATCH) to najpopularniejsza konwencja.ApplicationId— po opublikowaniu nie zmienisz go. Zmiana identyfikatora = nowa aplikacja w sklepie. Używaj formatu odwrotnej domeny.
Ikony i splash screen
.NET MAUI generuje ikony i splash screeny automatycznie z jednego pliku źródłowego (SVG lub PNG). Upewnij się, że masz to skonfigurowane:
<ItemGroup>
<!-- Ikona aplikacji — źródło SVG, 1024x1024 lub większe -->
<MauiIcon Include="Resources\AppIconppicon.svg"
ForegroundFile="Resources\AppIconppiconfg.svg"
Color="#512BD4" />
<!-- Splash screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg"
Color="#512BD4"
BaseSize="128,128" />
</ItemGroup>
App Store wymaga ikony co najmniej 1024×1024 pikseli, Google Play — 512×512. MAUI wygeneruje wszystkie potrzebne rozmiary sam, ale plik źródłowy musi być odpowiednio duży.
Publikacja na Android — Google Play
No dobra, zaczynamy od Androida, bo jest (odrobinę) prostszy. Cały proces składa się z trzech etapów: utworzenie keystore’a, zbudowanie podpisanej paczki AAB i przesłanie jej do Google Play Console.
Tworzenie keystore’a do podpisywania
Android wymaga, żeby każda aplikacja była podpisana kluczem kryptograficznym przed dystrybucją. Podczas developmentu .NET używa automatycznego klucza debug, ale do publikacji potrzebujesz własnego keystore’a:
# Generowanie keystore za pomocą keytool (z JDK)
keytool -genkey -v -keystore mojaaplikacja.keystore -alias MojaAplikacjaKey -keyalg RSA -keysize 2048 -validity 10000 -storepass TwojeHasloDoKeystore -keypass TwojeHasloDoKlucza
I teraz uwaga — to jest naprawdę ważne:
- Nigdy nie trać tego keystore’a. Jeśli go zgubisz, nie zaktualizujesz aplikacji w Google Play. Koniec. Zrób kopię zapasową w bezpiecznym miejscu — najlepiej w dwóch.
- Ustaw
-validityna wysoką wartość. 10000 dni to jakieś 27 lat. Google Play odrzuci keystore z krótkim okresem ważności. - Nie commituj keystore’a do repozytorium. Hasła trzymaj w zmiennych środowiskowych albo w menedżerze sekretów.
Budowanie podpisanej paczki AAB
Google Play wymaga formatu AAB (Android App Bundle), nie APK. AAB pozwala Google generować zoptymalizowane APK dla każdej konfiguracji urządzenia — dzięki temu użytkownik pobiera mniejszą paczkę.
# Budowanie podpisanej paczki AAB
dotnet publish -f net10.0-android -c Release -p:AndroidKeyStore=true -p:AndroidSigningKeyStore=mojaaplikacja.keystore -p:AndroidSigningKeyAlias=MojaAplikacjaKey -p:AndroidSigningKeyPass=env:ANDROID_KEY_PASS -p:AndroidSigningStorePass=env:ANDROID_STORE_PASS
Zwróć uwagę na prefiks env: przy hasłach. Dzięki niemu hasła są pobierane ze zmiennych środowiskowych zamiast wpisywania ich jawnie w komendzie. Można też użyć prefiksu file:, żeby wskazać plik z hasłem. Obie metody zapobiegają wyciekowi haseł do logów budowania.
Możesz też skonfigurować podpisywanie bezpośrednio w .csproj, żeby nie wklepywać tego wszystkiego za każdym razem:
<PropertyGroup Condition="
and '' == 'Release'">
<AndroidKeyStore>True</AndroidKeyStore>
<AndroidSigningKeyStore>mojaaplikacja.keystore</AndroidSigningKeyStore>
<AndroidSigningKeyAlias>MojaAplikacjaKey</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>env:ANDROID_KEY_PASS</AndroidSigningKeyPass>
<AndroidSigningStorePass>env:ANDROID_STORE_PASS</AndroidSigningStorePass>
</PropertyGroup>
Po budowaniu w katalogu wyjściowym znajdziesz dwa pliki AAB — jeden niepodpisany i jeden z sufiksem -signed. Do Google Play przesyłasz ten podpisany (wiem, brzmi oczywiste, ale uwierz mi — ludzie się mylą).
Wymagania Google Play — API Level
Google Play regularnie podnosi minimalny wymagany Target API Level. Na marzec 2026 nowe aplikacje muszą celować w Android 15 (API Level 35). W .NET MAUI 10 domyślny Target Framework (net10.0-android) automatycznie celuje w najnowszy wspierany API Level, więc zazwyczaj nie trzeba tego ręcznie ustawiać.
Ale jeśli potrzebujesz zmienić minimalną wersję Androida:
<PropertyGroup>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<!-- Minimalna wersja Androida (API 21 = Android 5.0) -->
</PropertyGroup>
Przesyłanie do Google Play Console
Ważna sprawa — pierwsze przesłanie AAB do Google Play musi być ręczne. Google musi powiązać sygnaturę klucza z Twoją aplikacją i tego nie da się obejść. Dopiero kolejne wersje możesz wysyłać automatycznie przez CI/CD.
Kroki w Google Play Console:
- Zaloguj się na play.google.com/console (potrzebujesz konta Google Play Developer — jednorazowa opłata 25 USD).
- Utwórz nową aplikację — podaj nazwę, język domyślny, typ i czy jest bezpłatna.
- Wypełnij wszystkie wymagane sekcje: opis, grafiki, klasyfikacja wiekowa, polityka prywatności.
- Przejdź do Release > Production (albo Testing > Internal testing, jeśli chcesz najpierw potestować).
- Prześlij podpisany plik AAB.
- Wyślij aplikację do recenzji.
Pierwsza recenzja trwa od kilku godzin do kilku dni. Kolejne aktualizacje idą znacznie szybciej.
Publikacja na iOS — App Store
A teraz iOS. Przygotuj się psychicznie, bo to jest ten trudniejszy etap. Publikacja na iOS wymaga konta Apple Developer (99 USD rocznie), certyfikatów podpisywania, provisioning profiles i — co kluczowe — komputera Mac do zbudowania paczki IPA. Nie da się tego obejść.
Certyfikaty i provisioning profiles
Ekosystem Apple opiera podpisywanie na trzech powiązanych elementach:
- Certyfikat Apple Distribution — identyfikuje Ciebie lub Twoją organizację. Ważny przez rok, potem trzeba odnowić.
- App ID — unikalny identyfikator aplikacji (np.
com.mojafirma.mojaaplikacja). Może być explicit albo wildcard (com.mojafirma.*). - Provisioning Profile — plik łączący certyfikat z App ID i mechanizmem dystrybucji. Bez niego aplikacja się po prostu nie zbuduje.
Wszystko konfigurujesz w Apple Developer Portal (developer.apple.com). Po utworzeniu provisioning profile pobierasz go i instalujesz na Macu.
Konfiguracja podpisywania w projekcie
W pliku .csproj dodaj konfigurację podpisywania dla iOS:
<PropertyGroup Condition="'' == 'Release'
And ">
<CodesignKey>Apple Distribution: Moja Firma (ABCDEFGH12)</CodesignKey>
<CodesignProvision>Nazwa_Provisioning_Profile</CodesignProvision>
<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>
<ArchiveOnBuild>true</ArchiveOnBuild>
</PropertyGroup>
Wartość CodesignKey to nazwa certyfikatu z Keychain Access na Macu. CodesignProvision to nazwa (albo UUID) provisioning profile.
Budowanie paczki IPA
Na Macu uruchamiasz:
# Budowanie IPA do dystrybucji App Store
dotnet publish -f net10.0-ios -c Release -p:RuntimeIdentifier=ios-arm64 -p:ArchiveOnBuild=true -p:CodesignKey="Apple Distribution: Moja Firma (ABCDEFGH12)" -p:CodesignProvision="MojaAplikacja_AppStore_Profile"
Plik .ipa znajdziesz w bin/Release/net10.0-ios/ios-arm64/publish/.
Przesyłanie do App Store Connect
Mając IPA, musisz ją przesłać do App Store Connect. Najwygodniejsza opcja to xcrun altool:
# Przesyłanie IPA do App Store Connect
xcrun altool --upload-app --type ios --file bin/Release/net10.0-ios/ios-arm64/publish/MojaAplikacja.ipa --apiKey TWOJ_API_KEY --apiIssuer TWOJ_ISSUER_ID
Alternatywnie, możesz użyć aplikacji Transporter (za darmo w Mac App Store) — ma interfejs graficzny, więc jest prostsza w obsłudze.
Po przesłaniu aplikacja pojawi się w App Store Connect. Stamtąd kierujesz ją do TestFlight (testy beta) albo bezpośrednio do recenzji App Store.
Privacy Manifest — obowiązkowy dla iOS
To jest element, który potrafi zaskoczyć. Od maja 2024 Apple wymaga od każdej aplikacji iOS pliku Privacy Manifest (PrivacyInfo.xcprivacy). Nie masz go? Aplikacja zostanie odrzucona. I dotyczy to wszystkich aplikacji .NET MAUI na iOS, bo sam runtime .NET korzysta z API objętych tym wymogiem.
Utwórz plik Platforms/iOS/PrivacyInfo.xcprivacy:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>C617.1</string></array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>35F9.1</string></array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>E174.1</string></array>
</dict>
</array>
</dict>
</plist>
Następnie dodaj go do .csproj:
<ItemGroup Condition=" == 'ios'">
<BundleResource Include="Platforms\iOS\PrivacyInfo.xcprivacy"
LogicalName="PrivacyInfo.xcprivacy" />
</ItemGroup>
Te trzy kategorie API (FileTimestamp, SystemBootTime, DiskSpace) to minimum wymagane przez runtime .NET. Jeśli Twoja aplikacja korzysta z dodatkowych API z listy Apple (np. UserDefaults, lokalizacja), musisz dopisać odpowiednie wpisy.
Optymalizacja rozmiaru aplikacji
Rozmiar aplikacji ma realne znaczenie. Użytkownicy na wolnym mobilnym internecie mogą po prostu zrezygnować z pobierania zbyt ciężkiej appki. Na szczęście .NET MAUI daje kilka narzędzi do odchudzenia paczki.
Trimming — usuwanie nieużywanego kodu
Trimming (przycinanie) automatycznie usuwa nieużywany kod z aplikacji i jej zależności. W buildach Release jest domyślnie włączony, ale możesz kontrolować jego agresywność:
<PropertyGroup Condition="'' == 'Release'">
<!-- Włączenie trimmingu (domyślnie true dla Release) -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Tryb trimmingu:
link = agresywny (usuwa nieużywane members)
copyused = konserwatywny (usuwa całe nieużywane assembly) -->
<TrimMode>link</TrimMode>
</PropertyGroup>
Tryb link jest bardziej agresywny i daje mniejsze paczki, ale bywa zbyt gorliwy — potrafi usunąć kod potrzebny przez refleksję. Jeśli po trimmingu aplikacja się wywala, dodaj atrybut [Preserve] do klas używanych refleksyjnie albo przełącz się na tryb copyused.
Targetowanie konkretnej architektury
Domyślnie .NET MAUI buduje paczki Android obsługujące cztery architektury (arm, arm64, x86, x64) — to po ~30 MB na każdą. Przy dystrybucji przez Google Play nie musisz się tym martwić, bo AAB automatycznie to ogarnia. Ale jeśli dystrybuujesz APK bezpośrednio, warto ograniczyć architektury:
<!-- Dla dystrybucji APK poza Google Play -->
<PropertyGroup Condition="">
<RuntimeIdentifiers>android-arm64</RuntimeIdentifiers>
</PropertyGroup>
Na iOS zawsze targetujesz ios-arm64 — symulator nie jest potrzebny w paczce dystrybucyjnej.
Compiled bindings — obowiązkowe przy trimmingu
Jeśli używasz data bindingu w XAML, zawsze ustawiaj x:DataType. Compiled bindings są 8–20× szybsze od refleksyjnych, a co ważniejsze — jedyne działające przy pełnym trimmingu i NativeAOT. Bindingi refleksyjne po przycięciu kodu po prostu przestają działać.
<!-- Zawsze deklaruj x:DataType -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MojaAplikacja.ViewModels"
x:DataType="vm:StronaGlownaViewModel">
<Label Text="{Binding Tytul}" />
</ContentPage>
Najczęstsze błędy przy publikacji i jak je naprawić
Przez lata pracy z .NET MAUI (i wcześniej Xamarin) widziałem sporo problemów przy publikacji. Oto te, które pojawiają się najczęściej.
Błąd „Unpacking failed” przy budowaniu na Android
Klasyka. Ten błąd pojawia się, bo wieloplatformowy system budowania próbuje przetworzyć zależności iOS podczas budowania dla Androida.
Rozwiązanie: Zawsze używaj dotnet publish z parametrem -f wskazującym konkretną platformę. Nigdy nie publikuj całego rozwiązania:
# Poprawnie — buduj dla konkretnej platformy
dotnet publish -f net10.0-android -c Release MojaAplikacja/MojaAplikacja.csproj
# Błędnie — nie publikuj rozwiązania
# dotnet publish MojaAplikacja.sln
Aplikacja crashuje po pobraniu z Google Play (ale lokalnie działa)
Ten problem potrafi doprowadzić do szału. Aplikacja działa idealnie na Twoim telefonie, ale po pobraniu ze sklepu się wysypuje. Winowajcą jest zwykle mechanizm Split APK — Google dzieli Twoje AAB na mniejsze APK dopasowane do urządzenia, co czasem psuje ładowanie natywnych bibliotek.
Co zrobić:
- Testuj AAB lokalnie za pomocą
bundletoolzanim prześlesz do Google Play. - Jeśli używasz NativeAOT, spróbuj tymczasowo go wyłączyć (
<RunAOTCompilation>false</RunAOTCompilation>). - Wyczyść katalogi
biniobji zbuduj od nowa. Serio, to rozwiązuje zaskakująco wiele problemów.
# Testowanie AAB lokalnie za pomocą bundletool
java -jar bundletool.jar build-apks --bundle=mojaaplikacja-signed.aab --output=mojaaplikacja.apks --ks=mojaaplikacja.keystore --ks-key-alias=MojaAplikacjaKey
# Instalacja na podłączonym urządzeniu
java -jar bundletool.jar install-apks --apks=mojaaplikacja.apks
„No iOS signing identities match” na Macu
Provisioning profile nie pasuje do żadnego certyfikatu w Keychain. Najczęstsze przyczyny:
- Certyfikat wygasł — sprawdź datę w Keychain Access.
- Profile wygenerowany z innym certyfikatem — utwórz nowy w Apple Developer Portal.
- Brakuje klucza prywatnego — jeśli przenosiłeś certyfikat z innego Maca, musisz wyeksportować go jako
.p12razem z kluczem prywatnym.
Duplikat nazw plików obrazów
Od .NET 8 system budowania sprawdza unikalność nazw grafik. Dwa obrazy o tej samej nazwie w różnych folderach? Błąd. Rozwiązanie: zmień nazwy albo użyj właściwości Link w .csproj.
Automatyzacja z CI/CD — GitHub Actions
Ręczna publikacja działa na początku, ale szybko staje się żmudna i podatna na pomyłki. GitHub Actions pozwala zautomatyzować cały proces. Oto przykładowy workflow dla Androida:
name: Publikacja Android
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Instalacja .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Instalacja workloadu MAUI
run: dotnet workload install maui-android
- name: Dekodowanie keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > mojaaplikacja.keystore
- name: Publikacja AAB
env:
ANDROID_KEY_PASS: ${{ secrets.ANDROID_KEY_PASS }}
ANDROID_STORE_PASS: ${{ secrets.ANDROID_STORE_PASS }}
run: |
dotnet publish -f net10.0-android -c Release -p:AndroidKeyStore=true -p:AndroidSigningKeyStore=mojaaplikacja.keystore -p:AndroidSigningKeyAlias=MojaAplikacjaKey -p:AndroidSigningKeyPass=env:ANDROID_KEY_PASS -p:AndroidSigningStorePass=env:ANDROID_STORE_PASS
- name: Upload artefaktu
uses: actions/upload-artifact@v4
with:
name: android-aab
path: "**/*-signed.aab"
Dla iOS potrzebujesz runnera macOS (runs-on: macos-latest) i dodatkowych kroków do instalacji certyfikatów i provisioning profiles. Warto sprawdzić akcję .NET MAUI - Apple Provisioning na GitHub Marketplace — potrafi sporo uprościć.
Lista kontrolna przed publikacją
Przed każdym przesłaniem do sklepu przejdź tę checklistę. Osobiście mam ją wydrukowana obok monitora:
- Wersjonowanie —
ApplicationVersionzwiększony,ApplicationDisplayVersionzaktualizowany. - Konfiguracja Release — buduj z
-c Release. Nigdy, przenigdy nie wysyłaj buildów Debug. - Podpisywanie — Android: keystore z poprawnym aliasem i hasłami. iOS: aktualny certyfikat + provisioning profile.
- Privacy Manifest —
PrivacyInfo.xcprivacyz wymaganymi kategoriami API (iOS). - Ikony — odpowiednia rozdzielczość (1024×1024 dla iOS, 512×512 dla Android).
- Trimming i compiled bindings —
x:DataTypena wszystkich stronach XAML. - Testy na fizycznym urządzeniu — build Release, nie Debug.
- API Level — Target API Level zgodny z wymaganiami Google Play (API 35+).
Za pierwszym razem ten cały proces wydaje się przytłaczający. Ale po dwóch-trzech iteracjach staje się rutyną. Kluczem jest dobre zrozumienie mechanizmów podpisywania, poprawna konfiguracja projektu i — przede wszystkim — automatyzacja CI/CD. Wtedy każda kolejna wersja trafia do sklepu jednym kliknięciem.
Najczęściej zadawane pytania (FAQ)
Czy do publikacji aplikacji .NET MAUI na iOS potrzebuję Maca?
Tak, niestety. Mac jest wymagany do budowania paczek IPA, bo narzędzia do kompilacji i podpisywania (Xcode, codesign) są dostępne wyłącznie na macOS. Możesz pisać kod na Windows i budować na zdalnym Macu (np. przez GitHub Actions z runnerem macos-latest), ale sam build musi odbyć się na macOS.
AAB czy APK — którego formatu użyć?
AAB (Android App Bundle) to format wymagany przez Google Play. Pozwala Google generować zoptymalizowane APK dla każdego urządzenia, zmniejszając rozmiar pobierania. APK to tradycyjny format do dystrybucji poza Google Play (ad-hoc, przez stronę). .NET MAUI generuje oba formaty w buildzie Release.
Ile kosztuje publikacja w Google Play i App Store?
Google Play Developer — jednorazowo 25 USD. Apple Developer Program — 99 USD rocznie. I tak, Apple trzeba płacić co roku, inaczej aplikacje znikają ze sklepu. Przesyłanie aktualizacji w obu przypadkach jest bezpłatne.
Dlaczego aplikacja działa lokalnie, ale crashuje po pobraniu z Google Play?
Najprawdopodobniej chodzi o mechanizm Split APK. Google dzieli AAB na mniejsze pakiety dopasowane do urządzenia, co czasem powoduje problemy z ładowaniem natywnych bibliotek. Przetestuj AAB lokalnie narzędziem bundletool i sprawdź logcat. Często pomaga wyczyszczenie bin/obj i ponowny build.
Czy mogę zaktualizować aplikację Xamarin.Forms używając .NET MAUI?
Tak — pod warunkiem, że zachowasz ten sam ApplicationId i podpiszesz AAB tym samym keystore’m. Google Play potraktuje to jako aktualizację, nie nową aplikację. Nie musisz zmieniać nazwy pakietu ani tworzyć nowego wpisu w sklepie.