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:
Claude 2025-11-19 03:44:38 +00:00
parent 865e0ff962
commit e4e5a03dd8
No known key found for this signature in database
13 changed files with 1720 additions and 0 deletions

6
.gitignore vendored
View File

@ -20,3 +20,9 @@ dist
# VSCode settings # VSCode settings
.vscode .vscode
# iOS
ios/Frameworks/
ios/xcuserdata/
ios/DerivedData/
*.xcframework

340
IOS.md Normal file
View File

@ -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

94
ios/.gitignore vendored Normal file
View File

@ -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

View File

@ -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 */;
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

185
ios/Memos/ContentView.swift Normal file
View File

@ -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)
}

58
ios/Memos/Info.plist Normal file
View File

@ -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>

19
ios/Memos/MemosApp.swift Normal file
View File

@ -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()
}
}
}
}

View File

@ -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
}
}

224
ios/README.md Normal file
View File

@ -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).

163
mobile/server.go Normal file
View File

@ -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")
}

39
scripts/build-ios.sh Executable file
View File

@ -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."