mirror of https://github.com/usememos/memos.git
feat(ios): add native iOS app support with gomobile
Implements a native iOS app that runs the full Memos backend locally on iOS devices. Architecture: - Uses gomobile to compile Go backend as iOS framework - SwiftUI app with WKWebView displays the React UI - All data stored locally in SQLite on device - Optional network access for other devices to connect Key Components: - mobile/server.go: gomobile binding layer for iOS - ios/Memos/: Native SwiftUI app with server management - scripts/build-ios.sh: Build script for iOS framework - IOS.md: Comprehensive iOS documentation Features: - Full backend runs natively on iOS (no cloud required) - Complete feature parity with desktop version - Network access toggle to allow LAN connections - Settings UI showing server status and network address - Automatic server lifecycle management Network Modes: - Local only (default): accessible only from the device - Network access: binds to 0.0.0.0 for LAN access Usage: 1. Run ./scripts/build-ios.sh to build framework 2. Open ios/Memos.xcodeproj in Xcode 3. Build and run on iOS device or simulator Technical Details: - Minimum iOS 15.0 - Server runs on port 5230 (configurable) - Data stored in app Documents directory - WKWebView for web UI rendering - Native iOS controls for settings See IOS.md and ios/README.md for detailed documentation.
This commit is contained in:
parent
865e0ff962
commit
e4e5a03dd8
|
|
@ -20,3 +20,9 @@ dist
|
|||
|
||||
# VSCode settings
|
||||
.vscode
|
||||
|
||||
# iOS
|
||||
ios/Frameworks/
|
||||
ios/xcuserdata/
|
||||
ios/DerivedData/
|
||||
*.xcframework
|
||||
|
|
|
|||
|
|
@ -0,0 +1,340 @@
|
|||
# Memos iOS App Guide
|
||||
|
||||
This guide explains how to build and run Memos as a native iOS application on your iPhone or iPad.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Build the iOS framework
|
||||
./scripts/build-ios.sh
|
||||
|
||||
# 2. Open in Xcode
|
||||
open ios/Memos.xcodeproj
|
||||
|
||||
# 3. Select your device and run (Cmd+R)
|
||||
```
|
||||
|
||||
## What is the iOS App?
|
||||
|
||||
The Memos iOS app runs the **full Memos backend server** directly on your iOS device, packaged as a native app. This means:
|
||||
|
||||
- ✅ All your data stays on your device
|
||||
- ✅ No internet connection required
|
||||
- ✅ Complete feature parity with desktop
|
||||
- ✅ Optional network access for other devices
|
||||
- ✅ Native iOS interface with SwiftUI
|
||||
|
||||
## How It Works
|
||||
|
||||
We use **gomobile** to compile the Go backend into an iOS framework that runs natively on iOS devices. The app displays the React web UI in a WKWebView, communicating with the local Go server.
|
||||
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ iOS Device │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ SwiftUI App │ │
|
||||
│ │ ├─ WKWebView (UI) │ │
|
||||
│ │ └─ Go Server │ │
|
||||
│ │ └─ SQLite DB │ │
|
||||
│ └──────────────────────┘ │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Local-First
|
||||
|
||||
All data is stored in SQLite on your device in the app's Documents directory. No cloud required.
|
||||
|
||||
### Network Access (Optional)
|
||||
|
||||
Enable "Allow Network Access" in settings to let other devices on your local network connect to your Memos instance:
|
||||
|
||||
1. Open Settings (⚙️ icon)
|
||||
2. Toggle "Allow Network Access"
|
||||
3. Share the displayed network URL with other devices
|
||||
|
||||
**Example**: If your iPhone's IP is `192.168.1.50`, other devices can access Memos at `http://192.168.1.50:5230`
|
||||
|
||||
### Full Feature Support
|
||||
|
||||
The iOS app runs the complete Memos backend, so all features work:
|
||||
|
||||
- ✅ Memo creation and editing
|
||||
- ✅ Markdown support
|
||||
- ✅ File attachments
|
||||
- ✅ Tags and search
|
||||
- ✅ User management
|
||||
- ✅ API access
|
||||
- ✅ RSS feeds
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- macOS with Xcode 15+
|
||||
- Go 1.21+
|
||||
- iOS device or simulator
|
||||
|
||||
### Step 1: Build the Framework
|
||||
|
||||
```bash
|
||||
./scripts/build-ios.sh
|
||||
```
|
||||
|
||||
This compiles the Go backend to `ios/Frameworks/Mobile.xcframework`. First build takes 5-10 minutes.
|
||||
|
||||
### Step 2: Configure Xcode
|
||||
|
||||
1. Open `ios/Memos.xcodeproj`
|
||||
2. Select the "Memos" project → "Signing & Capabilities"
|
||||
3. Choose your Apple Developer team
|
||||
4. Xcode will handle provisioning automatically
|
||||
|
||||
### Step 3: Build and Run
|
||||
|
||||
- Select your target device (iPhone/iPad or Simulator)
|
||||
- Press `Cmd+R` or click the Play button
|
||||
- App will install and launch automatically
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Backend Changes
|
||||
|
||||
After modifying Go code:
|
||||
|
||||
```bash
|
||||
# Rebuild the framework
|
||||
./scripts/build-ios.sh
|
||||
|
||||
# Rebuild in Xcode
|
||||
# (Cmd+B or Cmd+R)
|
||||
```
|
||||
|
||||
### Making iOS UI Changes
|
||||
|
||||
Edit Swift files in `ios/Memos/`:
|
||||
- `MemosApp.swift` - App entry point
|
||||
- `ContentView.swift` - Main UI and WebView
|
||||
- `ServerManager.swift` - Server control logic
|
||||
|
||||
Changes are reflected immediately on rebuild (Cmd+R).
|
||||
|
||||
### Debugging
|
||||
|
||||
View Go server logs in Xcode's debug console. To enable verbose logging, edit `ServerManager.swift`:
|
||||
|
||||
```swift
|
||||
// Change "prod" to "dev"
|
||||
let url = MobileNewServer(dataDir, port, addr, "dev", &serverError)
|
||||
```
|
||||
|
||||
## Architecture Details
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
ios/
|
||||
├── Memos/ # iOS app source
|
||||
│ ├── MemosApp.swift # SwiftUI app definition
|
||||
│ ├── ContentView.swift # Main view with WebView
|
||||
│ ├── ServerManager.swift # Go server interface
|
||||
│ ├── Assets.xcassets/ # Icons and images
|
||||
│ └── Info.plist # App configuration
|
||||
├── Memos.xcodeproj/ # Xcode project
|
||||
└── Frameworks/ # Generated (gitignored)
|
||||
└── Mobile.xcframework # Compiled Go backend
|
||||
|
||||
mobile/
|
||||
└── server.go # Go → iOS binding layer
|
||||
```
|
||||
|
||||
### How the Binding Works
|
||||
|
||||
The `mobile/server.go` package exposes a simple interface for iOS:
|
||||
|
||||
```go
|
||||
// Start server, returns URL
|
||||
func NewServer(dataDir, port, addr, mode string) (string, error)
|
||||
|
||||
// Stop server
|
||||
func StopServer() error
|
||||
|
||||
// Check if running
|
||||
func IsServerRunning() bool
|
||||
```
|
||||
|
||||
Swift code in `ServerManager.swift` calls these functions via the gomobile-generated framework.
|
||||
|
||||
### Data Storage
|
||||
|
||||
App data location: `Documents/memos-data/`
|
||||
|
||||
```
|
||||
Documents/
|
||||
└── memos-data/
|
||||
├── memos_prod.db # SQLite database
|
||||
└── assets/ # Uploaded files
|
||||
```
|
||||
|
||||
This directory is:
|
||||
- Persistent across app launches
|
||||
- Backed up to iCloud (if enabled)
|
||||
- Accessible via Files app (if configured)
|
||||
|
||||
## Network Access Details
|
||||
|
||||
### Localhost Mode (Default)
|
||||
|
||||
Server binds to `""` (empty string), accessible only from the device:
|
||||
- URL: `http://localhost:5230`
|
||||
- Only the iOS app can connect
|
||||
- No firewall or network configuration needed
|
||||
|
||||
### Network Mode (Optional)
|
||||
|
||||
Server binds to `0.0.0.0`, accessible from any device on the network:
|
||||
- URL: `http://<device-ip>:5230`
|
||||
- Any device on the same WiFi can connect
|
||||
- Requires local network permission (iOS 14+)
|
||||
|
||||
**Security Considerations:**
|
||||
- Only enable on trusted networks
|
||||
- Set a strong password in Memos
|
||||
- iOS will show "Local Network" permission prompt
|
||||
- Server stops when app is backgrounded
|
||||
|
||||
## Limitations
|
||||
|
||||
### Background Execution
|
||||
|
||||
iOS suspends apps in the background. The Memos server **stops** when you switch apps or lock your device.
|
||||
|
||||
**Workaround**: Keep the app in foreground or use Split View on iPad.
|
||||
|
||||
**Future**: Could use Background Modes for limited background execution.
|
||||
|
||||
### Network Availability
|
||||
|
||||
Other devices can only connect when:
|
||||
- App is in foreground
|
||||
- Device is awake
|
||||
- Network access is enabled
|
||||
- Both devices on same network
|
||||
|
||||
### Performance
|
||||
|
||||
Mobile hardware is less powerful than desktop. Expect:
|
||||
- Slower initial database migrations
|
||||
- Slightly slower search on large datasets
|
||||
- Limited by iOS memory constraints
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "gomobile: command not found"
|
||||
|
||||
Install gomobile:
|
||||
```bash
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
```
|
||||
|
||||
### Framework Build Fails
|
||||
|
||||
Ensure you have Go 1.21+ installed:
|
||||
```bash
|
||||
go version # Should show 1.21 or higher
|
||||
```
|
||||
|
||||
### Xcode Can't Find Framework
|
||||
|
||||
Make sure you ran `./scripts/build-ios.sh` first to generate `Mobile.xcframework`.
|
||||
|
||||
### Server Won't Start
|
||||
|
||||
Check Xcode console for errors. Common issues:
|
||||
- Data directory permissions
|
||||
- Database corruption (delete app and reinstall)
|
||||
- Insufficient storage
|
||||
|
||||
### Can't Connect from Other Devices
|
||||
|
||||
1. Verify "Allow Network Access" is ON
|
||||
2. Check both devices are on same WiFi network
|
||||
3. Try disabling VPN on client device
|
||||
4. Check firewall settings on client
|
||||
5. Verify iOS granted "Local Network" permission
|
||||
|
||||
### Blank WebView
|
||||
|
||||
- Wait 5-10 seconds for server startup
|
||||
- Check Xcode console for "Server started" message
|
||||
- Force quit and restart app
|
||||
- Clear app data (delete and reinstall)
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
**Q: Does this require an internet connection?**
|
||||
A: No, everything runs locally on your device.
|
||||
|
||||
**Q: Is my data uploaded to any cloud?**
|
||||
A: No, all data stays on your device unless you enable iCloud backup.
|
||||
|
||||
**Q: Can I use this with the desktop version?**
|
||||
A: They use separate databases. To sync, you'd need to set up manual export/import.
|
||||
|
||||
**Q: Does it work on iPad?**
|
||||
A: Yes, universal app supports iPhone and iPad.
|
||||
|
||||
**Q: Can multiple devices connect simultaneously?**
|
||||
A: Yes, when network access is enabled, any number of devices can connect.
|
||||
|
||||
**Q: What happens to the server when I background the app?**
|
||||
A: iOS suspends the app and server stops. It restarts when you return to the app.
|
||||
|
||||
**Q: Can I change the port number?**
|
||||
A: Currently hardcoded to 5230 for consistency. You can modify `ServerManager.swift` to change it.
|
||||
|
||||
**Q: How much storage does it use?**
|
||||
A: Base app is ~50MB. Database grows with your memos and attachments.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
- [ ] Background execution using Background Tasks framework
|
||||
- [ ] Bonjour/mDNS service discovery
|
||||
- [ ] Share extension for quick memo creation
|
||||
- [ ] Siri shortcuts integration
|
||||
- [ ] Home screen widgets
|
||||
- [ ] Apple Watch companion app
|
||||
- [ ] iCloud sync between multiple iOS devices
|
||||
- [ ] Export/import database backups
|
||||
- [ ] Face ID/Touch ID app lock
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to iOS app development:
|
||||
|
||||
1. Make your changes to `mobile/*.go` or `ios/Memos/*`
|
||||
2. Test on both iPhone and iPad simulators
|
||||
3. Test on physical device
|
||||
4. Submit PR with description of changes
|
||||
|
||||
## More Information
|
||||
|
||||
- Full iOS README: [ios/README.md](ios/README.md)
|
||||
- Main Memos docs: [CLAUDE.md](CLAUDE.md)
|
||||
- Build script: [scripts/build-ios.sh](scripts/build-ios.sh)
|
||||
- Mobile binding: [mobile/server.go](mobile/server.go)
|
||||
|
||||
## Support
|
||||
|
||||
For iOS-specific issues, please include:
|
||||
- iOS version
|
||||
- Device model
|
||||
- Xcode version
|
||||
- Go version
|
||||
- Error messages from Xcode console
|
||||
|
||||
File issues at: https://github.com/usememos/memos/issues
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
||||
|
||||
# Gomobile generated frameworks
|
||||
Frameworks/
|
||||
*.xcframework
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1A000001000000000000001 /* MemosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A000002000000000000001 /* MemosApp.swift */; };
|
||||
1A000003000000000000001 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A000004000000000000001 /* ContentView.swift */; };
|
||||
1A000005000000000000001 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A000006000000000000001 /* ServerManager.swift */; };
|
||||
1A000007000000000000001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A000008000000000000001 /* Assets.xcassets */; };
|
||||
1A000009000000000000001 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A00000A000000000000001 /* Mobile.xcframework */; };
|
||||
1A00000B000000000000001 /* Mobile.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1A00000A000000000000001 /* Mobile.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
1A00000C000000000000001 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
1A00000B000000000000001 /* Mobile.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1A00000D000000000000001 /* Memos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memos.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1A000002000000000000001 /* MemosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosApp.swift; sourceTree = "<group>"; };
|
||||
1A000004000000000000001 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
1A000006000000000000001 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; };
|
||||
1A000008000000000000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
1A00000A000000000000001 /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = Frameworks/Mobile.xcframework; sourceTree = "<group>"; };
|
||||
1A00000E000000000000001 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
1A00000F000000000000001 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1A000009000000000000001 /* Mobile.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1A000010000000000000001 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1A000011000000000000001 /* Memos */,
|
||||
1A000012000000000000001 /* Products */,
|
||||
1A000013000000000000001 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1A000011000000000000001 /* Memos */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1A000002000000000000001 /* MemosApp.swift */,
|
||||
1A000004000000000000001 /* ContentView.swift */,
|
||||
1A000006000000000000001 /* ServerManager.swift */,
|
||||
1A000008000000000000001 /* Assets.xcassets */,
|
||||
1A00000E000000000000001 /* Info.plist */,
|
||||
);
|
||||
path = Memos;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1A000012000000000000001 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1A00000D000000000000001 /* Memos.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1A000013000000000000001 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1A00000A000000000000001 /* Mobile.xcframework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
1A000014000000000000001 /* Memos */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 1A000015000000000000001 /* Build configuration list for PBXNativeTarget "Memos" */;
|
||||
buildPhases = (
|
||||
1A000016000000000000001 /* Sources */,
|
||||
1A00000F000000000000001 /* Frameworks */,
|
||||
1A000017000000000000001 /* Resources */,
|
||||
1A00000C000000000000001 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Memos;
|
||||
productName = Memos;
|
||||
productReference = 1A00000D000000000000001 /* Memos.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
1A000018000000000000001 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1500;
|
||||
LastUpgradeCheck = 1500;
|
||||
TargetAttributes = {
|
||||
1A000014000000000000001 = {
|
||||
CreatedOnToolsVersion = 15.0;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 1A000019000000000000001 /* Build configuration list for PBXProject "Memos" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 1A000010000000000000001;
|
||||
productRefGroup = 1A000012000000000000001 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
1A000014000000000000001 /* Memos */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
1A000017000000000000001 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1A000007000000000000001 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
1A000016000000000000001 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1A000003000000000000001 /* ContentView.swift in Sources */,
|
||||
1A000005000000000000001 /* ServerManager.swift in Sources */,
|
||||
1A000001000000000000001 /* MemosApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
1A00001A000000000000001 /* 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;
|
||||
};
|
||||
1A00001B000000000000001 /* 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;
|
||||
};
|
||||
1A00001C000000000000001 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Memos/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.usememos.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1A00001D000000000000001 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Memos/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.usememos.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
1A000015000000000000001 /* Build configuration list for PBXNativeTarget "Memos" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1A00001C000000000000001 /* Debug */,
|
||||
1A00001D000000000000001 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
1A000019000000000000001 /* Build configuration list for PBXProject "Memos" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1A00001A000000000000001 /* Debug */,
|
||||
1A00001B000000000000001 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 1A000018000000000000001 /* Project object */;
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var serverManager: ServerManager
|
||||
@State private var showSettings = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
if serverManager.isRunning, let url = serverManager.serverURL {
|
||||
WebView(url: URL(string: url)!)
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
} else if let error = serverManager.error {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.red)
|
||||
Text("Server Error")
|
||||
.font(.title)
|
||||
Text(error)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
Button("Retry") {
|
||||
serverManager.startServer()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.padding()
|
||||
} else {
|
||||
VStack(spacing: 20) {
|
||||
ProgressView()
|
||||
.scaleEffect(1.5)
|
||||
Text("Starting Memos Server...")
|
||||
.font(.title2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Text("Memos")
|
||||
.font(.headline)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: { showSettings = true }) {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView()
|
||||
.environmentObject(serverManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebView: UIViewRepresentable {
|
||||
let url: URL
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
webView.allowsBackForwardNavigationGestures = true
|
||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
|
||||
return webView
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
let request = URLRequest(url: url)
|
||||
webView.load(request)
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var serverManager: ServerManager
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section {
|
||||
HStack {
|
||||
Text("Server Status")
|
||||
Spacer()
|
||||
Circle()
|
||||
.fill(serverManager.isRunning ? Color.green : Color.red)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(serverManager.isRunning ? "Running" : "Stopped")
|
||||
.foregroundColor(serverManager.isRunning ? .green : .red)
|
||||
}
|
||||
|
||||
if serverManager.isRunning, let url = serverManager.serverURL {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Local URL")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(url)
|
||||
.font(.footnote)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Server")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Allow Network Access", isOn: $serverManager.allowNetworkAccess)
|
||||
|
||||
if serverManager.allowNetworkAccess {
|
||||
if let ipAddress = serverManager.getLocalIPAddress() {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Network Address")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text("http://\(ipAddress):\(5230)")
|
||||
.font(.footnote)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
Text("Other devices on your network can access Memos at the address above.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Text("Server is only accessible from this device.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Network")
|
||||
} footer: {
|
||||
Text("Enable network access to allow other devices on your local network to connect to your Memos instance.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Restart Server") {
|
||||
serverManager.stopServer()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
serverManager.startServer()
|
||||
}
|
||||
}
|
||||
|
||||
Button("Stop Server", role: .destructive) {
|
||||
serverManager.stopServer()
|
||||
dismiss()
|
||||
}
|
||||
.disabled(!serverManager.isRunning)
|
||||
} header: {
|
||||
Text("Actions")
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Port: 5230")
|
||||
Text("Database: SQLite")
|
||||
Text("Mode: Production")
|
||||
}
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} header: {
|
||||
Text("Configuration")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.environmentObject(ServerManager.shared)
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?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>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Memos</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>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<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>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Memos needs local network access to allow other devices to connect to your instance.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct MemosApp: App {
|
||||
@StateObject private var serverManager = ServerManager.shared
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(serverManager)
|
||||
.onAppear {
|
||||
serverManager.startServer()
|
||||
}
|
||||
.onDisappear {
|
||||
serverManager.stopServer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import Mobile // This will be the gomobile framework
|
||||
|
||||
class ServerManager: ObservableObject {
|
||||
static let shared = ServerManager()
|
||||
|
||||
@Published var isRunning = false
|
||||
@Published var serverURL: String?
|
||||
@Published var error: String?
|
||||
@Published var allowNetworkAccess = false {
|
||||
didSet {
|
||||
if oldValue != allowNetworkAccess && isRunning {
|
||||
// Restart server with new settings
|
||||
stopServer()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.startServer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let port: Int = 5230
|
||||
|
||||
private init() {}
|
||||
|
||||
func startServer() {
|
||||
guard !isRunning else { return }
|
||||
|
||||
do {
|
||||
// Get the documents directory
|
||||
let documentsPath = try FileManager.default.url(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true
|
||||
).path
|
||||
|
||||
let dataDir = MobileGetDataDirectory(documentsPath)
|
||||
|
||||
// Determine bind address based on network access setting
|
||||
let addr = allowNetworkAccess ? "0.0.0.0" : ""
|
||||
|
||||
var serverError: NSError?
|
||||
let url = MobileNewServer(dataDir, port, addr, "prod", &serverError)
|
||||
|
||||
if let error = serverError {
|
||||
throw error
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.serverURL = url
|
||||
self.isRunning = true
|
||||
self.error = nil
|
||||
}
|
||||
|
||||
print("Server started at: \(url ?? "unknown")")
|
||||
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.error = error.localizedDescription
|
||||
self.isRunning = false
|
||||
}
|
||||
print("Failed to start server: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func stopServer() {
|
||||
guard isRunning else { return }
|
||||
|
||||
do {
|
||||
var stopError: NSError?
|
||||
MobileStopServer(&stopError)
|
||||
|
||||
if let error = stopError {
|
||||
throw error
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isRunning = false
|
||||
self.serverURL = nil
|
||||
}
|
||||
|
||||
print("Server stopped")
|
||||
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
print("Failed to stop server: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIPAddress() -> String? {
|
||||
var address: String?
|
||||
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
||||
|
||||
guard getifaddrs(&ifaddr) == 0 else { return nil }
|
||||
guard let firstAddr = ifaddr else { return nil }
|
||||
|
||||
for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
|
||||
let interface = ifptr.pointee
|
||||
let addrFamily = interface.ifa_addr.pointee.sa_family
|
||||
|
||||
if addrFamily == UInt8(AF_INET) {
|
||||
let name = String(cString: interface.ifa_name)
|
||||
if name == "en0" { // WiFi interface
|
||||
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
|
||||
&hostname, socklen_t(hostname.count),
|
||||
nil, socklen_t(0), NI_NUMERICHOST)
|
||||
address = String(cString: hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddr)
|
||||
return address
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
# Memos iOS App
|
||||
|
||||
This directory contains the iOS app for Memos, allowing you to run your personal Memos instance directly on your iPhone or iPad.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Full Memos Backend**: Runs the complete Go backend server locally on your device
|
||||
- ✅ **Native iOS App**: SwiftUI-based native app with WKWebView for the web UI
|
||||
- ✅ **Network Access**: Optional network access to allow other devices to connect
|
||||
- ✅ **Offline First**: All data stored locally on your device using SQLite
|
||||
- ✅ **No Cloud Required**: Completely self-hosted on your iOS device
|
||||
|
||||
## Architecture
|
||||
|
||||
The iOS app uses `gomobile` to compile the Go backend as an iOS framework that runs natively on iOS:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ iOS App (SwiftUI) │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ WKWebView (React UI) │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ ServerManager (Swift) │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ Mobile Framework (Go/gomobile)│ │
|
||||
│ │ - HTTP/gRPC Server │ │
|
||||
│ │ - SQLite Database │ │
|
||||
│ │ - All Backend Logic │ │
|
||||
│ └──────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS with Xcode 15 or later
|
||||
- Go 1.21 or later
|
||||
- `gomobile` (will be installed automatically by build script)
|
||||
|
||||
## Building
|
||||
|
||||
### 1. Build the Go Framework
|
||||
|
||||
From the project root:
|
||||
|
||||
```bash
|
||||
chmod +x scripts/build-ios.sh
|
||||
./scripts/build-ios.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Install `gomobile` if not present
|
||||
- Compile the Go backend to an iOS framework (`Mobile.xcframework`)
|
||||
- Place the framework in `ios/Frameworks/`
|
||||
|
||||
The first build may take 5-10 minutes as it compiles the entire Go backend for iOS.
|
||||
|
||||
### 2. Open in Xcode
|
||||
|
||||
```bash
|
||||
open ios/Memos.xcodeproj
|
||||
```
|
||||
|
||||
### 3. Configure Code Signing
|
||||
|
||||
1. Select the "Memos" project in the navigator
|
||||
2. Select the "Memos" target
|
||||
3. Go to "Signing & Capabilities"
|
||||
4. Select your development team
|
||||
5. Xcode will automatically manage provisioning
|
||||
|
||||
### 4. Build and Run
|
||||
|
||||
- Select your iOS device or simulator from the scheme dropdown
|
||||
- Press `Cmd+R` to build and run
|
||||
|
||||
## Usage
|
||||
|
||||
### First Launch
|
||||
|
||||
1. Launch the app on your device
|
||||
2. The server will start automatically (may take a few seconds)
|
||||
3. The web UI will load in the app
|
||||
4. Complete the initial setup (create admin account)
|
||||
|
||||
### Network Access
|
||||
|
||||
To allow other devices on your network to access your Memos instance:
|
||||
|
||||
1. Tap the gear icon (⚙️) in the top-right
|
||||
2. Toggle "Allow Network Access" ON
|
||||
3. The server will restart and bind to `0.0.0.0`
|
||||
4. Your network address will be displayed (e.g., `http://192.168.1.100:5230`)
|
||||
5. Other devices can now access Memos at this address
|
||||
|
||||
**Security Note**: When network access is enabled, anyone on your local network can access your Memos instance. Ensure you're on a trusted network and set a strong password.
|
||||
|
||||
### Data Storage
|
||||
|
||||
All data is stored in your iOS app's Documents directory:
|
||||
|
||||
```
|
||||
Documents/
|
||||
└── memos-data/
|
||||
├── memos_prod.db # SQLite database
|
||||
└── assets/ # Uploaded files
|
||||
```
|
||||
|
||||
This data persists between app launches and is backed up to iCloud (if enabled).
|
||||
|
||||
## Configuration
|
||||
|
||||
The iOS app uses the following default settings:
|
||||
|
||||
- **Port**: 5230 (same as default Memos)
|
||||
- **Database**: SQLite (stored in app Documents)
|
||||
- **Mode**: Production
|
||||
- **Bind Address**:
|
||||
- `""` (localhost only) - default
|
||||
- `"0.0.0.0"` (all interfaces) - when network access enabled
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Errors
|
||||
|
||||
**"gomobile: command not found"**
|
||||
```bash
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
```
|
||||
|
||||
**Framework not found in Xcode**
|
||||
|
||||
Make sure you've run `./scripts/build-ios.sh` first to build the Go framework.
|
||||
|
||||
**Code signing errors**
|
||||
|
||||
Ensure you've selected a valid development team in Xcode project settings.
|
||||
|
||||
### Runtime Issues
|
||||
|
||||
**Server fails to start**
|
||||
|
||||
Check the Xcode console for error messages. Common issues:
|
||||
- Data directory permissions
|
||||
- Port already in use (unlikely on iOS)
|
||||
- Database migration errors
|
||||
|
||||
**Can't access from other devices**
|
||||
|
||||
1. Ensure "Allow Network Access" is enabled
|
||||
2. Check that devices are on the same network
|
||||
3. Try disabling VPN on either device
|
||||
4. Check firewall settings (on the client device)
|
||||
|
||||
**WebView shows blank page**
|
||||
|
||||
- Wait a few seconds for the server to fully start
|
||||
- Check Xcode console for server startup messages
|
||||
- Try force-quitting and restarting the app
|
||||
|
||||
## Development
|
||||
|
||||
### Rebuilding the Framework
|
||||
|
||||
After making changes to the Go backend:
|
||||
|
||||
```bash
|
||||
./scripts/build-ios.sh
|
||||
```
|
||||
|
||||
Then rebuild the iOS app in Xcode (Cmd+B).
|
||||
|
||||
### Debugging
|
||||
|
||||
Go logs are printed to the Xcode console. You can view them in Xcode's debug console when running the app.
|
||||
|
||||
To enable more verbose logging, change the mode to "dev" in `ServerManager.swift`:
|
||||
|
||||
```swift
|
||||
let url = MobileNewServer(dataDir, port, addr, "dev", &serverError)
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
ios/
|
||||
├── Memos/ # iOS app source
|
||||
│ ├── MemosApp.swift # App entry point
|
||||
│ ├── ContentView.swift # Main UI with WebView
|
||||
│ ├── ServerManager.swift # Go server interface
|
||||
│ ├── Assets.xcassets/ # App icons and assets
|
||||
│ └── Info.plist # App configuration
|
||||
├── Memos.xcodeproj/ # Xcode project
|
||||
├── Frameworks/ # Generated frameworks (gitignored)
|
||||
│ └── Mobile.xcframework # Go backend framework
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Background Execution**: iOS suspends apps in the background. The server stops when the app is backgrounded.
|
||||
- **Network Access**: Requires devices to be on the same local network
|
||||
- **Performance**: May be slower than desktop due to mobile hardware constraints
|
||||
- **Database Size**: Limited by available iOS storage
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Possible improvements for the iOS app:
|
||||
|
||||
- [ ] Background server execution (using background modes)
|
||||
- [ ] Local network service discovery (Bonjour)
|
||||
- [ ] Share extension for quick memo capture
|
||||
- [ ] Siri shortcuts integration
|
||||
- [ ] Widget support
|
||||
- [ ] Watch app companion
|
||||
- [ ] iCloud sync between devices
|
||||
- [ ] Export/import database
|
||||
|
||||
## License
|
||||
|
||||
Same as the main Memos project (MIT License).
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package mobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/usememos/memos/internal/profile"
|
||||
"github.com/usememos/memos/internal/version"
|
||||
"github.com/usememos/memos/server"
|
||||
"github.com/usememos/memos/store"
|
||||
"github.com/usememos/memos/store/db"
|
||||
)
|
||||
|
||||
// ServerConfig holds the configuration for the mobile server.
|
||||
type ServerConfig struct {
|
||||
// DataDir is the directory where memos data will be stored
|
||||
DataDir string
|
||||
// Port is the port to bind the server to
|
||||
Port int
|
||||
// Addr is the address to bind to (use "0.0.0.0" for network access, "" for localhost only)
|
||||
Addr string
|
||||
// Mode can be "prod", "dev", or "demo"
|
||||
Mode string
|
||||
}
|
||||
|
||||
// MobileServer wraps the memos server for use in mobile applications.
|
||||
type MobileServer struct {
|
||||
server *server.Server
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.Mutex
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
globalServer *MobileServer
|
||||
serverMu sync.Mutex
|
||||
)
|
||||
|
||||
// NewServer creates a new mobile server instance with the given configuration.
|
||||
// Returns the server URL on success.
|
||||
func NewServer(dataDir string, port int, addr string, mode string) (string, error) {
|
||||
serverMu.Lock()
|
||||
defer serverMu.Unlock()
|
||||
|
||||
if globalServer != nil {
|
||||
return "", fmt.Errorf("server already running")
|
||||
}
|
||||
|
||||
// Set up logger
|
||||
logLevel := slog.LevelInfo
|
||||
if mode == "dev" {
|
||||
logLevel = slog.LevelDebug
|
||||
}
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: logLevel,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
// Ensure data directory exists
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
// Create profile
|
||||
instanceProfile := &profile.Profile{
|
||||
Mode: mode,
|
||||
Addr: addr,
|
||||
Port: port,
|
||||
Data: dataDir,
|
||||
Driver: "sqlite",
|
||||
DSN: "",
|
||||
Version: version.GetCurrentVersion(mode),
|
||||
}
|
||||
|
||||
if err := instanceProfile.Validate(); err != nil {
|
||||
return "", fmt.Errorf("invalid profile: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Create database driver
|
||||
dbDriver, err := db.NewDBDriver(instanceProfile)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return "", fmt.Errorf("failed to create db driver: %w", err)
|
||||
}
|
||||
|
||||
// Create store and migrate
|
||||
storeInstance := store.New(dbDriver, instanceProfile)
|
||||
if err := storeInstance.Migrate(ctx); err != nil {
|
||||
cancel()
|
||||
return "", fmt.Errorf("failed to migrate: %w", err)
|
||||
}
|
||||
|
||||
// Create server
|
||||
s, err := server.NewServer(ctx, instanceProfile, storeInstance)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return "", fmt.Errorf("failed to create server: %w", err)
|
||||
}
|
||||
|
||||
globalServer = &MobileServer{
|
||||
server: s,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Start server in background
|
||||
go func() {
|
||||
if err := s.Start(ctx); err != nil {
|
||||
logger.Error("server error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Construct server URL
|
||||
serverURL := fmt.Sprintf("http://localhost:%d", port)
|
||||
if addr != "" && addr != "localhost" && addr != "127.0.0.1" {
|
||||
serverURL = fmt.Sprintf("http://%s:%d", addr, port)
|
||||
}
|
||||
|
||||
logger.Info("Memos server started", "url", serverURL)
|
||||
|
||||
return serverURL, nil
|
||||
}
|
||||
|
||||
// StopServer stops the running server.
|
||||
func StopServer() error {
|
||||
serverMu.Lock()
|
||||
defer serverMu.Unlock()
|
||||
|
||||
if globalServer == nil {
|
||||
return fmt.Errorf("no server running")
|
||||
}
|
||||
|
||||
globalServer.mu.Lock()
|
||||
defer globalServer.mu.Unlock()
|
||||
|
||||
globalServer.logger.Info("Stopping memos server")
|
||||
globalServer.server.Shutdown(globalServer.ctx)
|
||||
globalServer.cancel()
|
||||
globalServer = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsServerRunning returns true if the server is currently running.
|
||||
func IsServerRunning() bool {
|
||||
serverMu.Lock()
|
||||
defer serverMu.Unlock()
|
||||
return globalServer != nil
|
||||
}
|
||||
|
||||
// GetDataDirectory returns an appropriate data directory for iOS.
|
||||
// This should be called from Swift/Objective-C with the app's document directory.
|
||||
func GetDataDirectory(documentsDir string) string {
|
||||
return filepath.Join(documentsDir, "memos-data")
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building Memos iOS Framework..."
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if gomobile is installed
|
||||
if ! command -v gomobile &> /dev/null; then
|
||||
echo -e "${YELLOW}gomobile not found. Installing...${NC}"
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
fi
|
||||
|
||||
# Navigate to project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Clean previous builds
|
||||
echo -e "${YELLOW}Cleaning previous builds...${NC}"
|
||||
rm -rf ios/Frameworks/Mobile.xcframework
|
||||
|
||||
# Build the mobile framework
|
||||
echo -e "${YELLOW}Building Go mobile framework...${NC}"
|
||||
gomobile bind -target=ios -o ios/Frameworks/Mobile.xcframework ./mobile
|
||||
|
||||
echo -e "${GREEN}✓ iOS Framework built successfully!${NC}"
|
||||
echo ""
|
||||
echo "Framework location: ios/Frameworks/Mobile.xcframework"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Open ios/Memos.xcodeproj in Xcode"
|
||||
echo "2. Connect your iOS device or select a simulator"
|
||||
echo "3. Build and run the project (Cmd+R)"
|
||||
echo ""
|
||||
echo "Note: The first build may take several minutes as it compiles the Go backend."
|
||||
Loading…
Reference in New Issue