IOS codes

This commit is contained in:
Sarto
2026-05-07 13:51:59 +03:30
parent e3b6e77dd3
commit 6c799d9e7f
27 changed files with 2069 additions and 42 deletions
+8
View File
@@ -30,3 +30,11 @@ todo.md
tmp
.claude
# iOS build artifacts
ios/Mobile.xcframework
ios/build
ios/DerivedData
ios/*.xcuserdata
ios/Thefeed.xcodeproj/xcuserdata
ios/Thefeed.xcodeproj/project.xcworkspace/xcuserdata
+42 -1
View File
@@ -1,4 +1,5 @@
.PHONY: all build build-server build-client test clean lint fmt vet
.PHONY: all build build-server build-client test clean lint fmt vet \
ios-bind ios-bind-catalyst ios-build ios-test ios-clean ios-list-sims ios-deps
BINARY_SERVER = thefeed-server
BINARY_CLIENT = thefeed-client
@@ -97,6 +98,46 @@ build-android-arm:
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=arm GOARM=7 go build $(call CLIENT_GOFLAGS,thefeed-client-android-arm) -o $(BUILD_DIR)/$(BINARY_CLIENT)-android-arm ./cmd/client
# ===== iOS / Mac Catalyst =====
# Requires: Xcode + gomobile (go install golang.org/x/mobile/cmd/gomobile@latest && gomobile init)
IOS_DIR = ios
IOS_FRAMEWORK = $(IOS_DIR)/Mobile.xcframework
IOS_SCHEME = Thefeed
IOS_PROJECT = $(IOS_DIR)/Thefeed.xcodeproj
# Default simulator: pick the first available iPhone (override with IOS_SIM_NAME='iPhone 17').
IOS_SIM_NAME ?= $(shell xcrun simctl list devices available 2>/dev/null | awk -F'[()]' '/-- iOS [0-9]/{ios=1;next} /^-- /{ios=0} ios && /iPhone/{print $$1; exit}' | sed 's/^[[:space:]]*//;s/[[:space:]]*$$//')
ios-deps:
@grep -q "golang.org/x/mobile" go.mod || go get golang.org/x/mobile/bind golang.org/x/mobile/bind/objc
go mod tidy
ios-bind: ios-deps
@command -v gomobile >/dev/null 2>&1 || { echo "gomobile not found. Run: go install golang.org/x/mobile/cmd/gomobile@latest && gomobile init"; exit 1; }
gomobile bind -iosversion=14.0 -target=ios,iossimulator -o $(IOS_FRAMEWORK) ./mobile
ios-bind-catalyst: ios-deps
@command -v gomobile >/dev/null 2>&1 || { echo "gomobile not found"; exit 1; }
gomobile bind -iosversion=14.0 -target=ios,iossimulator,maccatalyst -o $(IOS_FRAMEWORK) ./mobile
ios-list-sims:
xcrun simctl list devices available
ios-build: $(IOS_FRAMEWORK)
xcodebuild -project $(IOS_PROJECT) -scheme $(IOS_SCHEME) \
-destination 'platform=iOS Simulator,name=$(IOS_SIM_NAME)' \
build
ios-test: $(IOS_FRAMEWORK)
xcodebuild test -project $(IOS_PROJECT) -scheme $(IOS_SCHEME) \
-destination 'platform=iOS Simulator,name=$(IOS_SIM_NAME)'
$(IOS_FRAMEWORK):
$(MAKE) ios-bind
ios-clean:
rm -rf $(IOS_FRAMEWORK) $(IOS_DIR)/build $(IOS_DIR)/DerivedData
# UPX compression (requires upx in PATH) — only for Linux/Windows binaries
upx:
@command -v upx >/dev/null 2>&1 || { echo "upx not found, skipping compression"; exit 0; }
+13
View File
@@ -362,6 +362,19 @@ chmod +x thefeed-client
برنامه در اولین اجرا اجازه‌ی Battery Optimization Exemption را می‌گیرد تا سرویس پس‌زمینه توسط سیستم بسته نشود.
### iOS
نسخه‌ی iOS در حال توسعه است. سورس در پوشه‌ی [`ios/`](ios/) قرار دارد. برای ساخت روی مک:
```
go install golang.org/x/mobile/cmd/gomobile@latest && gomobile init
make ios-bind # ساخت Mobile.xcframework
make ios-build # بیلد روی iOS Simulator
make ios-test # اجرای تست‌ها
```
سپس `ios/Thefeed.xcodeproj` را در Xcode باز کنید.
## ⚙️ تنظیمات DNS
شما به **دو رکورد DNS** نیاز دارید. فرض کنید IP سرور شما `203.0.113.10` است:
+26
View File
@@ -12,6 +12,7 @@ DNS-based feed reader for Telegram channels and public X accounts. Designed for
sudo bash -c "$(curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh)"
```
- **Android APK** (Android 7.0+): pick `arm64-v8a` for any phone newer than ~2017, `armeabi-v7a` for older 32-bit-only devices.
- **iOS** (iOS 14+): App Store build planned. Source under [ios/](ios/) — see [iOS development](#ios-development) below.
Public configs to test with: [@thefeedconfig](https://t.me/thefeedconfig).
@@ -529,6 +530,31 @@ make fmt # Format code
make clean # Remove build artifacts
```
## iOS development
Wraps the Go client as a gomobile-bound xcframework consumed by a SwiftUI app under `ios/`. Server runs in-process on `127.0.0.1:<random-port>`; foreground only (iOS does not allow long-lived background servers).
Prereqs on macOS: Xcode 15+, Go 1.22+, gomobile.
```
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
```
Common targets:
```
make ios-bind # build Mobile.xcframework (iOS device + Simulator)
make ios-bind-catalyst # also include Mac Catalyst slice
make ios-build # build the app for the Simulator
make ios-test # run unit tests on the Simulator
make ios-list-sims # list available simulator destinations
```
Override the default simulator with `IOS_SIM_NAME='iPhone 16'`.
Open `ios/Thefeed.xcodeproj` in Xcode after `make ios-bind` to run from Xcode.
## Releases (GitHub Actions)
Pushing a tag that starts with `v` triggers CI build + GitHub Release.
+8 -7
View File
@@ -6,9 +6,10 @@ require (
github.com/gotd/td v0.142.0
github.com/miekg/dns v1.1.72
github.com/refraction-networking/utls v1.6.7
golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0
golang.org/x/term v0.41.0
golang.org/x/crypto v0.50.0
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b
golang.org/x/net v0.53.0
golang.org/x/term v0.42.0
)
require (
@@ -40,11 +41,11 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
rsc.io/qr v0.2.0 // indirect
)
+16 -14
View File
@@ -81,26 +81,28 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+10 -5
View File
@@ -265,13 +265,18 @@ func TestPersistScanResultsRescanOverwrites(t *testing.T) {
}
}
func TestPersistScanResultsNoSelectedList(t *testing.T) {
func TestPersistScanResultsSeedsDefaultListOnFirstRun(t *testing.T) {
s := newTestServerWithProfiles(t, &ProfileList{})
// No-op when there's no selected list to write into.
s.persistScanResultsToList([]string{"a:53"})
s.persistScanResultsToList([]string{"a:53", "b:53"})
pl := loadProfilesT(t, s)
if len(pl.ActiveLists) != 0 {
t.Errorf("ActiveLists got created: %v", pl.ActiveLists)
if len(pl.ActiveLists) != 1 || pl.ActiveLists[0].Name != defaultListName {
t.Fatalf("ActiveLists = %v, want one Default list", pl.ActiveLists)
}
if got := pl.ActiveLists[0].Resolvers; len(got) != 2 {
t.Errorf("Default list = %v, want 2 entries", got)
}
if pl.SelectedList != defaultListName {
t.Errorf("SelectedList = %q, want %q", pl.SelectedList, defaultListName)
}
}
+47 -12
View File
@@ -202,12 +202,12 @@
}
.sidebar-search {
padding: 7px 12px;
padding: 5px 10px;
border: none;
border-radius: 18px;
border-radius: 14px;
background: var(--surface);
color: var(--text);
font-size: 13px;
font-size: 12px;
outline: none;
width: 100%
}
@@ -959,7 +959,11 @@
background: var(--bg);
display: none;
flex-direction: column;
color: var(--text)
color: var(--text);
padding-top: var(--safe-top);
padding-bottom: var(--safe-bottom);
padding-left: var(--safe-left);
padding-right: var(--safe-right)
}
.tm-modal.active { display: flex }
@@ -3271,9 +3275,9 @@
<div class="modal">
<h2 id="profileEditorTitle" data-i18n="new_profile">New Profile</h2>
<div id="peWarning" class="tg-warning"></div>
<div class="form-group"><label data-i18n="nickname">Nickname</label><input id="peNick" maxlength="32" placeholder="My Server">
<div class="form-group"><label data-i18n="nickname">Nickname</label><input id="peNick" maxlength="32" placeholder="My Server" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div class="form-group"><label data-i18n="domain">Domain</label><input id="peDomain" placeholder="t.example.com">
<div class="form-group"><label data-i18n="domain">Domain</label><input id="peDomain" placeholder="t.example.com" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
<div class="form-group"><label data-i18n="passphrase">Passphrase</label><input type="password" id="peKey"
placeholder="..."></div>
@@ -4030,7 +4034,15 @@
poll_placeholder: 'Poll (open Telegram to view)',
}
};
var lang = localStorage.getItem('thefeed_lang') || 'fa';
// Order: native bridge (iOS/Android) → localStorage → 'fa'.
// iOS localStorage is per-origin (host:port) so picking up the
// native value first survives an embedded server rebinding to a
// new loopback port on each launch.
var lang = (function () {
try { if (typeof IOS !== 'undefined' && IOS.getLang) { var v = IOS.getLang(); if (v) return v; } } catch (e) { }
try { if (typeof Android !== 'undefined' && Android.getLang) { var v2 = Android.getLang(); if (v2) return v2; } } catch (e) { }
return localStorage.getItem('thefeed_lang') || 'fa';
})();
function t(k) { return (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k }
function applyLang() {
var isRtl = lang === 'fa';
@@ -4051,6 +4063,7 @@
localStorage.setItem('thefeed_lang', l);
applyLang();
if (typeof Android !== 'undefined') try { Android.setLang(l) } catch (e) { }
if (typeof IOS !== 'undefined' && IOS.setLang) try { IOS.setLang(l) } catch (e) { }
fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lang: l }) }).catch(function () { });
}
@@ -4126,7 +4139,10 @@
});
function filterChannels() {
var q = document.getElementById('channelSearch').value.toLowerCase();
document.querySelectorAll('.ch-item').forEach(function (el) { el.style.display = el.dataset.name.toLowerCase().includes(q) ? 'flex' : 'none' });
document.querySelectorAll('.ch-item').forEach(function (el) {
var hay = (el.dataset.name + ' ' + (el.dataset.label || '')).toLowerCase();
el.style.display = hay.includes(q) ? 'flex' : 'none';
});
}
// ===== INIT =====
@@ -5552,7 +5568,7 @@
// No 8.8.8.8 / 1.1.1.1 fallback — an empty resolver list in
// the URI is the sender's deliberate choice (e.g. they want
// the recipient to bring their own bank).
sharedNick = sharedNick.replace(/[\x00-\x1f\x7f]/g, '').trim().slice(0, 32);
sharedNick = sanitizeNickname(sharedNick);
// Only ask about bank merging when the URI actually carried
// resolvers — empty list means the sender shared zero, so
// there's nothing to merge and no prompt to show.
@@ -5664,12 +5680,31 @@
} catch (e) { showToast(e.message) }
}
// sanitizeNickname extracts a clean alias from raw input. If the user
// pasted a thefeed:// URI, pulls the n= param. Strips control chars,
// the clipboard emoji, newlines, and trims to 32 chars.
function sanitizeNickname(raw) {
var v = String(raw || '');
var m = v.match(/thefeed:\/\/[^\s?#]+(?:\?[^\s#]*)?(?:[?&]n=([^&\s#]*))/i);
if (m && m[1]) {
try { v = decodeURIComponent(m[1].replace(/\+/g, ' ')); } catch (e) { v = m[1]; }
}
// iOS Quick Type appends UI text starting with the clipboard glyph
// (U+1F4CB) when the user manually pastes — cut from there.
var clipIdx = v.indexOf('📋');
if (clipIdx >= 0) v = v.slice(0, clipIdx);
// Strip control / zero-width / bidi / BOM characters.
v = v.replace(/[\u0000-\u001F\u007F\u200B-\u200F\u2028\u2029\uFEFF]/g, "");
v = v.replace(/\s+/g, ' ').trim();
return v.slice(0, 32);
}
async function saveProfile() {
var errEl = document.getElementById('peError'); errEl.style.display = 'none';
// Trim and clamp the nickname server-side too: the maxlength
// attribute on the input only stops keyboard typing, not paste
// / programmatic value setting.
var nick = document.getElementById('peNick').value.trim().slice(0, 32);
var nick = sanitizeNickname(document.getElementById('peNick').value);
var domain = document.getElementById('peDomain').value.trim();
var key = document.getElementById('peKey').value;
if (!domain || !key) { errEl.textContent = t('domain') + ' / ' + t('passphrase'); errEl.style.display = 'block'; return }
@@ -7757,7 +7792,7 @@
document.getElementById('progressPanel').innerHTML = '';
var p = document.getElementById('progressPanel');
p.innerHTML = '<div class="progress-item" id="prog-init" data-last-update="' + Date.now() + '"><button class="progress-close" onclick="this.parentNode.remove()" title="Dismiss">&times;</button><div class="progress-label">' + t('loading') + '</div><div class="progress-bar"><div class="progress-fill" style="width:30%;animation:prog-pulse 1.5s ease-in-out infinite"></div></div></div>';
if (mobileQuery.matches) { openChat(); openLog(); }
if (mobileQuery.matches) { openChat(); }
}
function startAutoRefresh() { if (autoRefreshTimer) return; autoRefreshTimer = setInterval(function () { if (selectedChannel > 0) doRefresh(true) }, 600000) }
function updateNextFetchDisplay() {
@@ -8734,4 +8769,4 @@
<!-- END telemirror -->
</body>
</html>
</html>
+18 -3
View File
@@ -13,6 +13,7 @@ import (
"io/fs"
"log"
mrand "math/rand/v2"
"net"
"net/http"
"os"
"path/filepath"
@@ -301,8 +302,15 @@ func New(dataDir string, port int, host string, password string) (*Server, error
return s, nil
}
// Run starts the web server.
func (s *Server) Run() error {
// Run starts the web server, binding to s.host:s.port.
func (s *Server) Run() error { return s.serve(nil) }
// Serve runs the web server on an already-bound listener. Used by the
// mobile entry where the listener is opened first to discover the
// kernel-assigned port.
func (s *Server) Serve(ln net.Listener) error { return s.serve(ln) }
func (s *Server) serve(ln net.Listener) error {
mux := http.NewServeMux()
staticSub, _ := fs.Sub(staticFS, "static")
@@ -417,6 +425,9 @@ func (s *Server) Run() error {
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 30 * time.Minute,
}
if ln != nil {
return srv.Serve(ln)
}
return srv.ListenAndServe()
}
@@ -3008,7 +3019,11 @@ func (s *Server) persistScanResultsToList(healthy []string) {
}
list := findList(pl, pl.SelectedList)
if list == nil {
return
// First scan with no lists yet — seed a Default list so the
// UI doesn't show empty after the very first scan completes.
pl.ActiveLists = append(pl.ActiveLists, ActiveList{Name: defaultListName})
list = &pl.ActiveLists[len(pl.ActiveLists)-1]
pl.SelectedList = defaultListName
}
// Don't shrink a populated list on routine periodic checks.
if !overwrite && len(list.Resolvers) > 0 {
+512
View File
@@ -0,0 +1,512 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
E1000000000000000000E001 /* ThefeedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D001 /* ThefeedApp.swift */; };
E1000000000000000000E002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D002 /* ContentView.swift */; };
E1000000000000000000E003 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D003 /* WebView.swift */; };
E1000000000000000000E004 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D004 /* Bridge.swift */; };
E1000000000000000000E005 /* ServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D005 /* ServerController.swift */; };
E1000000000000000000E006 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D007 /* Assets.xcassets */; };
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E1000000000000000000E009 /* ThefeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D010 /* ThefeedTests.swift */; };
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
E1000000000000000000E011 /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D012 /* LanguagePicker.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
B1000000000000000000B005 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
D1000000000000000000D001 /* ThefeedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedApp.swift; sourceTree = "<group>"; };
D1000000000000000000D002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
D1000000000000000000D003 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
D1000000000000000000D004 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
D1000000000000000000D005 /* ServerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerController.swift; sourceTree = "<group>"; };
D1000000000000000000D006 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D1000000000000000000D007 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D1000000000000000000D008 /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = Mobile.xcframework; sourceTree = "<group>"; };
D1000000000000000000D009 /* Thefeed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Thefeed.app; sourceTree = BUILT_PRODUCTS_DIR; };
D1000000000000000000D010 /* ThefeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedTests.swift; sourceTree = "<group>"; };
D1000000000000000000D011 /* ThefeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ThefeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D1000000000000000000D012 /* LanguagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B1000000000000000000B003 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B1000000000000000000B008 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A1000000000000000000A002 /* Thefeed (root) */ = {
isa = PBXGroup;
children = (
A1000000000000000000A004 /* Thefeed */,
A1000000000000000000A006 /* ThefeedTests */,
A1000000000000000000A005 /* Frameworks */,
A1000000000000000000A003 /* Products */,
);
sourceTree = "<group>";
};
A1000000000000000000A003 /* Products */ = {
isa = PBXGroup;
children = (
D1000000000000000000D009 /* Thefeed.app */,
D1000000000000000000D011 /* ThefeedTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
A1000000000000000000A004 /* Thefeed */ = {
isa = PBXGroup;
children = (
D1000000000000000000D001 /* ThefeedApp.swift */,
D1000000000000000000D002 /* ContentView.swift */,
D1000000000000000000D003 /* WebView.swift */,
D1000000000000000000D004 /* Bridge.swift */,
D1000000000000000000D005 /* ServerController.swift */,
D1000000000000000000D012 /* LanguagePicker.swift */,
D1000000000000000000D007 /* Assets.xcassets */,
D1000000000000000000D006 /* Info.plist */,
);
path = Thefeed;
sourceTree = "<group>";
};
A1000000000000000000A006 /* ThefeedTests */ = {
isa = PBXGroup;
children = (
D1000000000000000000D010 /* ThefeedTests.swift */,
);
path = ThefeedTests;
sourceTree = "<group>";
};
A1000000000000000000A005 /* Frameworks */ = {
isa = PBXGroup;
children = (
D1000000000000000000D008 /* Mobile.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B1000000000000000000B001 /* Thefeed */ = {
isa = PBXNativeTarget;
buildConfigurationList = C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */;
buildPhases = (
B1000000000000000000B002 /* Sources */,
B1000000000000000000B003 /* Frameworks */,
B1000000000000000000B004 /* Resources */,
B1000000000000000000B005 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Thefeed;
productName = Thefeed;
productReference = D1000000000000000000D009 /* Thefeed.app */;
productType = "com.apple.product-type.application";
};
B1000000000000000000B006 /* ThefeedTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */;
buildPhases = (
B1000000000000000000B007 /* Sources */,
B1000000000000000000B008 /* Frameworks */,
);
buildRules = (
);
dependencies = (
F1000000000000000000F001 /* PBXTargetDependency */,
);
name = ThefeedTests;
productName = ThefeedTests;
productReference = D1000000000000000000D011 /* ThefeedTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXTargetDependency section */
F1000000000000000000F001 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B1000000000000000000B001 /* Thefeed */;
targetProxy = F1000000000000000000F002 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXContainerItemProxy section */
F1000000000000000000F002 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = A1000000000000000000A001 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B1000000000000000000B001;
remoteInfo = Thefeed;
};
/* End PBXContainerItemProxy section */
/* Begin PBXProject section */
A1000000000000000000A001 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1530;
TargetAttributes = {
B1000000000000000000B001 = {
CreatedOnToolsVersion = 15.3;
};
B1000000000000000000B006 = {
CreatedOnToolsVersion = 15.3;
TestTargetID = B1000000000000000000B001;
};
};
};
buildConfigurationList = C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = A1000000000000000000A002;
productRefGroup = A1000000000000000000A003 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
B1000000000000000000B001 /* Thefeed */,
B1000000000000000000B006 /* ThefeedTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
B1000000000000000000B004 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E006 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
B1000000000000000000B002 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E001 /* ThefeedApp.swift in Sources */,
E1000000000000000000E002 /* ContentView.swift in Sources */,
E1000000000000000000E003 /* WebView.swift in Sources */,
E1000000000000000000E004 /* Bridge.swift in Sources */,
E1000000000000000000E005 /* ServerController.swift in Sources */,
E1000000000000000000E011 /* LanguagePicker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B1000000000000000000B007 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E009 /* ThefeedTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
C1000000000000000000C003 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
C1000000000000000000C004 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
C1000000000000000000C005 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Thefeed/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
C1000000000000000000C006 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Thefeed/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
C1000000000000000000C008 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
};
name = Debug;
};
C1000000000000000000C009 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C1000000000000000000C003 /* Debug */,
C1000000000000000000C004 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C1000000000000000000C005 /* Debug */,
C1000000000000000000C006 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C1000000000000000000C008 /* Debug */,
C1000000000000000000C009 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = A1000000000000000000A001 /* Project object */;
}
+512
View File
@@ -0,0 +1,512 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
E1000000000000000000E001 /* ThefeedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D001 /* ThefeedApp.swift */; };
E1000000000000000000E002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D002 /* ContentView.swift */; };
E1000000000000000000E003 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D003 /* WebView.swift */; };
E1000000000000000000E004 /* Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D004 /* Bridge.swift */; };
E1000000000000000000E005 /* ServerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D005 /* ServerController.swift */; };
E1000000000000000000E006 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D007 /* Assets.xcassets */; };
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
E1000000000000000000E009 /* ThefeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D010 /* ThefeedTests.swift */; };
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D008 /* Mobile.xcframework */; };
E1000000000000000000E011 /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1000000000000000000D012 /* LanguagePicker.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
B1000000000000000000B005 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
E1000000000000000000E008 /* Mobile.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
D1000000000000000000D001 /* ThefeedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedApp.swift; sourceTree = "<group>"; };
D1000000000000000000D002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
D1000000000000000000D003 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
D1000000000000000000D004 /* Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridge.swift; sourceTree = "<group>"; };
D1000000000000000000D005 /* ServerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerController.swift; sourceTree = "<group>"; };
D1000000000000000000D006 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D1000000000000000000D007 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D1000000000000000000D008 /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = Mobile.xcframework; sourceTree = "<group>"; };
D1000000000000000000D009 /* Thefeed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Thefeed.app; sourceTree = BUILT_PRODUCTS_DIR; };
D1000000000000000000D010 /* ThefeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThefeedTests.swift; sourceTree = "<group>"; };
D1000000000000000000D011 /* ThefeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ThefeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D1000000000000000000D012 /* LanguagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B1000000000000000000B003 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E007 /* Mobile.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B1000000000000000000B008 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E010 /* Mobile.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A1000000000000000000A002 /* Thefeed (root) */ = {
isa = PBXGroup;
children = (
A1000000000000000000A004 /* Thefeed */,
A1000000000000000000A006 /* ThefeedTests */,
A1000000000000000000A005 /* Frameworks */,
A1000000000000000000A003 /* Products */,
);
sourceTree = "<group>";
};
A1000000000000000000A003 /* Products */ = {
isa = PBXGroup;
children = (
D1000000000000000000D009 /* Thefeed.app */,
D1000000000000000000D011 /* ThefeedTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
A1000000000000000000A004 /* Thefeed */ = {
isa = PBXGroup;
children = (
D1000000000000000000D001 /* ThefeedApp.swift */,
D1000000000000000000D002 /* ContentView.swift */,
D1000000000000000000D003 /* WebView.swift */,
D1000000000000000000D004 /* Bridge.swift */,
D1000000000000000000D005 /* ServerController.swift */,
D1000000000000000000D012 /* LanguagePicker.swift */,
D1000000000000000000D007 /* Assets.xcassets */,
D1000000000000000000D006 /* Info.plist */,
);
path = Thefeed;
sourceTree = "<group>";
};
A1000000000000000000A006 /* ThefeedTests */ = {
isa = PBXGroup;
children = (
D1000000000000000000D010 /* ThefeedTests.swift */,
);
path = ThefeedTests;
sourceTree = "<group>";
};
A1000000000000000000A005 /* Frameworks */ = {
isa = PBXGroup;
children = (
D1000000000000000000D008 /* Mobile.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B1000000000000000000B001 /* Thefeed */ = {
isa = PBXNativeTarget;
buildConfigurationList = C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */;
buildPhases = (
B1000000000000000000B002 /* Sources */,
B1000000000000000000B003 /* Frameworks */,
B1000000000000000000B004 /* Resources */,
B1000000000000000000B005 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Thefeed;
productName = Thefeed;
productReference = D1000000000000000000D009 /* Thefeed.app */;
productType = "com.apple.product-type.application";
};
B1000000000000000000B006 /* ThefeedTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */;
buildPhases = (
B1000000000000000000B007 /* Sources */,
B1000000000000000000B008 /* Frameworks */,
);
buildRules = (
);
dependencies = (
F1000000000000000000F001 /* PBXTargetDependency */,
);
name = ThefeedTests;
productName = ThefeedTests;
productReference = D1000000000000000000D011 /* ThefeedTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXTargetDependency section */
F1000000000000000000F001 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B1000000000000000000B001 /* Thefeed */;
targetProxy = F1000000000000000000F002 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXContainerItemProxy section */
F1000000000000000000F002 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = A1000000000000000000A001 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B1000000000000000000B001;
remoteInfo = Thefeed;
};
/* End PBXContainerItemProxy section */
/* Begin PBXProject section */
A1000000000000000000A001 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1530;
TargetAttributes = {
B1000000000000000000B001 = {
CreatedOnToolsVersion = 15.3;
};
B1000000000000000000B006 = {
CreatedOnToolsVersion = 15.3;
TestTargetID = B1000000000000000000B001;
};
};
};
buildConfigurationList = C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = A1000000000000000000A002;
productRefGroup = A1000000000000000000A003 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
B1000000000000000000B001 /* Thefeed */,
B1000000000000000000B006 /* ThefeedTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
B1000000000000000000B004 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E006 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
B1000000000000000000B002 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E001 /* ThefeedApp.swift in Sources */,
E1000000000000000000E002 /* ContentView.swift in Sources */,
E1000000000000000000E003 /* WebView.swift in Sources */,
E1000000000000000000E004 /* Bridge.swift in Sources */,
E1000000000000000000E005 /* ServerController.swift in Sources */,
E1000000000000000000E011 /* LanguagePicker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B1000000000000000000B007 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E1000000000000000000E009 /* ThefeedTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
C1000000000000000000C003 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
C1000000000000000000C004 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
C1000000000000000000C005 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Thefeed/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
C1000000000000000000C006 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Thefeed/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
C1000000000000000000C008 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
};
name = Debug;
};
C1000000000000000000C009 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.thefeed.ios.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Thefeed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Thefeed";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C1000000000000000000C001 /* Build configuration list for PBXProject "Thefeed" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C1000000000000000000C003 /* Debug */,
C1000000000000000000C004 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C1000000000000000000C002 /* Build configuration list for PBXNativeTarget "Thefeed" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C1000000000000000000C005 /* Debug */,
C1000000000000000000C006 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C1000000000000000000C007 /* Build configuration list for PBXNativeTarget "ThefeedTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C1000000000000000000C008 /* Debug */,
C1000000000000000000C009 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = A1000000000000000000A001 /* Project object */;
}
@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B1000000000000000000B001"
BuildableName = "Thefeed.app"
BlueprintName = "Thefeed"
ReferencedContainer = "container:Thefeed.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B1000000000000000000B006"
BuildableName = "ThefeedTests.xctest"
BlueprintName = "ThefeedTests"
ReferencedContainer = "container:Thefeed.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B1000000000000000000B006"
BuildableName = "ThefeedTests.xctest"
BlueprintName = "ThefeedTests"
ReferencedContainer = "container:Thefeed.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B1000000000000000000B001"
BuildableName = "Thefeed.app"
BlueprintName = "Thefeed"
ReferencedContainer = "container:Thefeed.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B1000000000000000000B001"
BuildableName = "Thefeed.app"
BlueprintName = "Thefeed"
ReferencedContainer = "container:Thefeed.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "image.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+190
View File
@@ -0,0 +1,190 @@
import Foundation
import Photos
import UIKit
import WebKit
/// Receives WKScriptMessage actions from `window.IOS.*` and routes
/// outbound navigations: loopback stays in the WebView, anything else
/// hands off to Safari.
final class Bridge: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
weak var webView: WKWebView?
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard
let body = message.body as? [String: Any],
let action = body["action"] as? String
else { return }
switch action {
case "saveMedia": save(body)
case "shareMedia": share(body)
case "openMedia": share(body) // iOS treats open and share via the same picker
case "setLang": setLang(body)
default: break
}
}
// MARK: - Navigation
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// Keep loopback inside the WebView; everything else goes to Safari.
if let host = url.host, host == "127.0.0.1" || host == "localhost" {
decisionHandler(.allow)
return
}
if navigationAction.navigationType == .linkActivated || url.scheme == "https" || url.scheme == "http" {
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
private func save(_ body: [String: Any]) {
guard let url = decode(body) else { return }
let mime = (body["mime"] as? String) ?? ""
if mime.hasPrefix("image/") {
saveImage(at: url)
return
}
if mime.hasPrefix("video/") {
saveVideo(at: url)
return
}
// Fallback for non-media (PDFs, archives, etc.) share sheet so
// the user picks Files / a third-party app.
present(url: url, save: false)
}
private func share(_ body: [String: Any]) {
guard let url = decode(body) else { return }
present(url: url, save: false)
}
// MARK: - Save to Photos
private func saveImage(at url: URL) {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else {
toast("Save failed: cannot decode image")
return
}
requestPhotoAdd { [weak self] granted in
guard granted else { self?.toast("Photo library access denied"); return }
UIImageWriteToSavedPhotosAlbum(image, self, #selector(Bridge.didFinishSavingImage(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
private func saveVideo(at url: URL) {
requestPhotoAdd { [weak self] granted in
guard granted else { self?.toast("Photo library access denied"); return }
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
}, completionHandler: { ok, err in
DispatchQueue.main.async {
self?.toast(ok ? "Saved to Photos" : "Save failed: \(err?.localizedDescription ?? "unknown")")
}
})
}
}
@objc private func didFinishSavingImage(
_ image: UIImage,
didFinishSavingWithError error: NSError?,
contextInfo: UnsafeRawPointer
) {
DispatchQueue.main.async { [weak self] in
self?.toast(error == nil ? "Saved to Photos" : "Save failed: \(error!.localizedDescription)")
}
}
private func requestPhotoAdd(_ handler: @escaping (Bool) -> Void) {
let status = PHPhotoLibrary.authorizationStatus(for: .addOnly)
if status == .authorized || status == .limited {
handler(true); return
}
if status == .denied || status == .restricted {
handler(false); return
}
PHPhotoLibrary.requestAuthorization(for: .addOnly) { st in
DispatchQueue.main.async { handler(st == .authorized || st == .limited) }
}
}
private func toast(_ msg: String) {
webView?.evaluateJavaScript(
"window.showToast && window.showToast(\(jsString(msg)))",
completionHandler: nil
)
}
private func jsString(_ s: String) -> String {
let escaped = s
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
return "\"\(escaped)\""
}
// MARK: - Language
private func setLang(_ body: [String: Any]) {
guard let lang = body["lang"] as? String else { return }
UserDefaults.standard.set(lang, forKey: "tf.lang")
}
private func decode(_ body: [String: Any]) -> URL? {
guard
let b64 = body["body"] as? String,
let data = Data(base64Encoded: b64),
let name = (body["name"] as? String).flatMap(safeName)
else { return nil }
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("share", isDirectory: true)
try? FileManager.default.createDirectory(
at: dir, withIntermediateDirectories: true
)
let url = dir.appendingPathComponent(name)
do {
try data.write(to: url, options: .atomic)
return url
} catch {
return nil
}
}
private func present(url: URL, save: Bool) {
DispatchQueue.main.async { [weak self] in
guard
let scene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first,
let root = scene.windows.first?.rootViewController
else { return }
let activities: [UIActivity]? = nil
let vc = UIActivityViewController(
activityItems: [url],
applicationActivities: activities
)
// iPad popover anchor.
vc.popoverPresentationController?.sourceView = self?.webView
root.present(vc, animated: true)
}
}
private func safeName(_ s: String) -> String? {
let bad = CharacterSet(charactersIn: "/\\:*?\"<>|\0")
let cleaned = s.components(separatedBy: bad).joined(separator: "_")
return cleaned.isEmpty ? nil : cleaned
}
}
+25
View File
@@ -0,0 +1,25 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var server: ServerController
@AppStorage("tf.lang") private var lang: String = ""
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
if !lang.isEmpty && server.port > 0 {
WebView(url: URL(string: "http://127.0.0.1:\(server.port)")!)
.ignoresSafeArea()
} else if let err = server.lastError {
VStack(spacing: 12) {
Text("startup failed").font(.headline).foregroundColor(.white)
Text(err).font(.caption).foregroundColor(.secondary)
Button("retry") { server.start() }
}
.padding()
} else {
ProgressView()
}
}
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>TheFeed</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!-- Loopback HTTP for the embedded server. NSAllowsLocalNetworking
keeps cleartext blocked for everything else. -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<!-- Required for saving downloaded images/videos to Photos. -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>thefeed needs to save downloaded images and videos to your Photos library.</string>
</dict>
</plist>
+68
View File
@@ -0,0 +1,68 @@
import SwiftUI
/// First-launch language selection. Stored in UserDefaults under "tf.lang".
struct LanguagePickerView: View {
let onPick: (String) -> Void
var body: some View {
ZStack {
Color(red: 0.07, green: 0.09, blue: 0.13).ignoresSafeArea()
VStack(spacing: 40) {
Spacer()
VStack(spacing: 8) {
Text("TheFeed")
.font(.system(size: 40, weight: .bold, design: .rounded))
.foregroundColor(.white)
Text("Choose your language · زبان خود را انتخاب کنید")
.font(.callout)
.foregroundColor(.white.opacity(0.6))
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
}
VStack(spacing: 16) {
LanguageButton(label: "English", subtitle: "Continue in English") {
onPick("en")
}
LanguageButton(label: "فارسی", subtitle: "ادامه به زبان فارسی") {
onPick("fa")
}
}
.padding(.horizontal, 32)
Spacer()
}
}
}
}
private struct LanguageButton: View {
let label: String
let subtitle: String
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 4) {
Text(label)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Text(subtitle)
.font(.footnote)
.foregroundColor(.white.opacity(0.7))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.white.opacity(0.08))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.white.opacity(0.15), lineWidth: 1)
)
)
}
}
}
+68
View File
@@ -0,0 +1,68 @@
import Foundation
import Mobile
import UIKit
/// Owns the embedded gomobile-backed HTTP server.
/// Restarts on foreground and stops on background since iOS doesn't
/// permit a long-lived background server.
final class ServerController: ObservableObject {
@Published private(set) var port: Int = 0
@Published private(set) var lastError: String?
private var instance: MobileServer?
private var observers: [NSObjectProtocol] = []
init() {
let center = NotificationCenter.default
observers.append(center.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil, queue: .main
) { [weak self] _ in self?.stop() })
observers.append(center.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil, queue: .main
) { [weak self] _ in self?.start() })
}
deinit {
observers.forEach(NotificationCenter.default.removeObserver)
instance?.stop()
}
func start() {
guard instance == nil else { return }
do {
let dir = try Self.dataDir()
var err: NSError?
guard let s = MobileNewServer(dir.path, &err) else {
lastError = err?.localizedDescription ?? "server start failed"
return
}
instance = s
port = Int(s.port())
lastError = nil
} catch {
lastError = error.localizedDescription
}
}
func stop() {
instance?.stop()
instance = nil
port = 0
}
private static func dataDir() throws -> URL {
let docs = try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
let dir = docs.appendingPathComponent("thefeeddata", isDirectory: true)
try FileManager.default.createDirectory(
at: dir, withIntermediateDirectories: true
)
return dir
}
}
+24
View File
@@ -0,0 +1,24 @@
import SwiftUI
@main
struct ThefeedApp: App {
@StateObject private var server = ServerController()
@AppStorage("tf.lang") private var lang: String = ""
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(server)
.onAppear { server.start() }
.onDisappear { server.stop() }
.fullScreenCover(isPresented: Binding(
get: { lang.isEmpty },
set: { _ in }
)) {
LanguagePickerView { picked in
lang = picked
}
}
}
}
}
+76
View File
@@ -0,0 +1,76 @@
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: URL
func makeCoordinator() -> Bridge { Bridge() }
func makeUIView(context: Context) -> WKWebView {
let cfg = WKWebViewConfiguration()
cfg.websiteDataStore = .default()
cfg.allowsInlineMediaPlayback = true
cfg.mediaTypesRequiringUserActionForPlayback = []
let userContent = WKUserContentController()
userContent.add(context.coordinator, name: "thefeed")
cfg.userContentController = userContent
let view = WKWebView(frame: .zero, configuration: cfg)
view.allowsBackForwardNavigationGestures = true
view.scrollView.bounces = true
view.navigationDelegate = context.coordinator
context.coordinator.webView = view
userContent.addUserScript(WKUserScript(
source: shimSource(),
injectionTime: .atDocumentStart,
forMainFrameOnly: true
))
view.load(URLRequest(url: url))
return view
}
private func shimSource() -> String {
let lang = resolveLang()
let langJS = lang.replacingOccurrences(of: "\"", with: "\\\"")
return """
window.IOS = {
isIOS: true,
_lang: "\(langJS)",
getLang: function() { return this._lang; },
setLang: function(l) {
this._lang = l;
window.webkit.messageHandlers.thefeed.postMessage(
{ action: 'setLang', lang: l });
},
saveMedia: function(b64, mime, name) {
window.webkit.messageHandlers.thefeed.postMessage(
{ action: 'saveMedia', body: b64, mime: mime, name: name });
},
shareMedia: function(b64, mime, name) {
window.webkit.messageHandlers.thefeed.postMessage(
{ action: 'shareMedia', body: b64, mime: mime, name: name });
},
openMedia: function(b64, mime, name) {
window.webkit.messageHandlers.thefeed.postMessage(
{ action: 'openMedia', body: b64, mime: mime, name: name });
}
};
"""
}
/// Reads the language picked at first launch. Falls back to "en"
/// for the brief window before the picker has been answered.
private func resolveLang() -> String {
let saved = UserDefaults.standard.string(forKey: "tf.lang") ?? ""
return saved.isEmpty ? "en" : saved
}
func updateUIView(_ view: WKWebView, context: Context) {
if view.url != url {
view.load(URLRequest(url: url))
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import XCTest
import Mobile
final class ThefeedTests: XCTestCase {
func testRejectsEmptyDir() {
var err: NSError?
let s = MobileNewServer("", &err)
XCTAssertNil(s)
XCTAssertNotNil(err)
}
func testServeAndStop() throws {
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("thefeed-test-\(UUID().uuidString)")
try FileManager.default.createDirectory(
at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
var err: NSError?
guard let server = MobileNewServer(dir.path, &err) else {
XCTFail("start failed: \(err?.localizedDescription ?? "?")")
return
}
XCTAssertGreaterThan(server.port(), 0)
let url = URL(string: "http://127.0.0.1:\(server.port())/api/status")!
let exp = expectation(description: "status")
let task = URLSession.shared.dataTask(with: url) { _, resp, _ in
if let http = resp as? HTTPURLResponse {
XCTAssertEqual(http.statusCode, 200)
}
exp.fulfill()
}
task.resume()
wait(for: [exp], timeout: 10)
server.stop()
}
}
+82
View File
@@ -0,0 +1,82 @@
// Package mobile is the gomobile-bind entry point used by the iOS
// (and Mac Catalyst) Swift app. It wraps internal/web.Server so the
// HTTP server runs in-process — iOS does not allow bundled child
// executables, so the Android subprocess approach can't be reused.
package mobile
import (
"errors"
"net"
"sync"
"github.com/sartoopjj/thefeed/internal/web"
)
// Server is a running thefeed-client instance bound to 127.0.0.1.
type Server struct {
web *web.Server
ln net.Listener
port int
mu sync.Mutex
stopped bool
doneErr error
done chan struct{}
}
// NewServer starts a server on a kernel-assigned port. dataDir must be
// a writable, app-private directory (e.g. NSDocumentDirectory on iOS).
func NewServer(dataDir string) (*Server, error) {
if dataDir == "" {
return nil, errors.New("mobile: dataDir is empty")
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
port := ln.Addr().(*net.TCPAddr).Port
ws, err := web.New(dataDir, port, "127.0.0.1", "")
if err != nil {
_ = ln.Close()
return nil, err
}
s := &Server{
web: ws,
ln: ln,
port: port,
done: make(chan struct{}),
}
go func() {
err := ws.Serve(ln)
s.mu.Lock()
s.doneErr = err
s.mu.Unlock()
close(s.done)
}()
return s, nil
}
// Port returns the listening port (0 after Stop).
func (s *Server) Port() int {
if s == nil {
return 0
}
return s.port
}
// Stop closes the listener and waits for serve to return.
func (s *Server) Stop() {
if s == nil {
return
}
s.mu.Lock()
if s.stopped {
s.mu.Unlock()
return
}
s.stopped = true
s.mu.Unlock()
_ = s.ln.Close()
<-s.done
}
+89
View File
@@ -0,0 +1,89 @@
package mobile
import (
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
)
func TestNewServerEmptyDir(t *testing.T) {
if _, err := NewServer(""); err == nil {
t.Errorf("NewServer(\"\") succeeded, want error")
}
}
func TestServerLifecycle(t *testing.T) {
dir := t.TempDir()
s, err := NewServer(dir)
if err != nil {
t.Fatalf("NewServer: %v", err)
}
defer s.Stop()
if s.Port() <= 0 {
t.Fatalf("Port() = %d, want > 0", s.Port())
}
url := fmt.Sprintf("http://127.0.0.1:%d/api/status", s.Port())
resp, err := pollGet(url, 5*time.Second)
if err != nil {
t.Fatalf("GET %s: %v", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("status = %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "version") {
t.Errorf("body = %q, expected to contain 'version'", string(body))
}
}
func TestStopIsIdempotent(t *testing.T) {
s, err := NewServer(t.TempDir())
if err != nil {
t.Fatalf("NewServer: %v", err)
}
s.Stop()
s.Stop() // must not panic
}
func TestStopReleasesPort(t *testing.T) {
dir := t.TempDir()
s, err := NewServer(dir)
if err != nil {
t.Fatalf("NewServer: %v", err)
}
url := fmt.Sprintf("http://127.0.0.1:%d/api/status", s.Port())
if _, err := pollGet(url, 3*time.Second); err != nil {
t.Fatalf("server never came up: %v", err)
}
s.Stop()
// After Stop the listener is closed — a fresh request must fail.
c := http.Client{Timeout: 500 * time.Millisecond}
if resp, err := c.Get(url); err == nil {
resp.Body.Close()
t.Errorf("server still answering after Stop")
}
}
// pollGet retries until the server is up, since the Serve goroutine
// may take a moment to start accepting on the listener.
func pollGet(url string, total time.Duration) (*http.Response, error) {
deadline := time.Now().Add(total)
c := http.Client{Timeout: 500 * time.Millisecond}
var lastErr error
for time.Now().Before(deadline) {
resp, err := c.Get(url)
if err == nil {
return resp, nil
}
lastErr = err
time.Sleep(50 * time.Millisecond)
}
return nil, lastErr
}
+12
View File
@@ -0,0 +1,12 @@
//go:build gomobiletools
package mobile
// Pins golang.org/x/mobile/bind* in go.mod so `go mod tidy` doesn't
// drop them between gomobile-bind invocations. The build tag keeps
// these out of normal compilation; the imports exist solely for go
// modules' dependency graph.
import (
_ "golang.org/x/mobile/bind"
_ "golang.org/x/mobile/bind/objc"
)