Skip to content

Repository files navigation

OpenDocument.ios

It's Android's first OpenOffice Document Reader... for iOS!

This is an iOS frontend for our C++ OpenDocument.core library.

Setup

Open OpenDocumentReader.xcodeproj in Xcode. Everything comes from Swift Package Manager and is resolved by Xcode — odrcore included, as the prebuilt OdrCoreObjC.xcframework the OdrCore package downloads from its release. There is no conan step and no C++ toolchain to set up.

To try an unreleased odrcore, point the package reference at a local checkout and build the xcframework there:

cd ../OpenDocument.core
apple/build_xcframework.py slice && apple/build_xcframework.py assemble

Its Package.swift then takes ODR_XCFRAMEWORK=OdrCoreObjC.xcframework from the environment of every xcodebuild invocation instead of the release artifact.

How a document reaches the screen

CoreWrapper hands the file to odrcore, which returns an HtmlService: a handle that knows which views the document has but has not rendered any of them. That service is connected to odrcore's HTTP server, bound to 127.0.0.1 on whichever port was free, and the web view is pointed at http://127.0.0.1:<port>/file/<prefix>/<page>.html. odrcore renders a page when the web view asks for it, on one of the server's threads.

The same thing OpenDocument.droid does, and for the same reasons: rendering happens off the thread that opened the document, only the pages that are looked at are rendered at all, and turning a page is a navigation rather than another translation. The <prefix> changes on every translation, because the web view caches by URL and a document re-translated after a password or an edit has to land on an address it has not seen.

There is no file-writing fallback: a socket that cannot be opened fails the translate, and the document is reported as failed. The web view still loads file: URLs, but only for the formats odrcore does not handle at all, which DocumentViewController hands it directly.

None of this needs a capability or prompts the user. A listening socket on loopback takes no entitlement, and the local network permission introduced in iOS 14 covers the local subnet and multicast, not 127.0.0.1. It does need an App Transport Security exception, since ATS blocks plain HTTP: NSAllowsLocalNetworking in both Info.plists, which is the narrow one for local addresses and — unlike NSAllowsArbitraryLoads — needs no justification in App Store review.

Formatting

Swift sources are formatted with swift-format from the active Xcode toolchain, configured in .swift-format. Run scripts/format.sh before committing; CI runs scripts/format.sh --check and fails on any difference.

Continuous integration

workflow what it does
format scripts/format.sh --check, on every push and pull request
build_test unit tests on the simulator plus a device build of both flavors
release upload to App Store Connect, by hand, see below

format needs nothing but the Xcode toolchain and reports style breakage in a minute, so it is kept apart from the build.

Releasing

The release workflow uploads a build to App Store Connect. It is dispatched by hand, and never submits for review, so promoting a build stays a deliberate step in App Store Connect:

gh workflow run release.yml -f version=1.38

It runs as three jobs:

job what it does
build one run producing both signed .ipas, archived on the run
upload one job per app, uploading its .ipa
record once both landed: tag the build, draft the GitHub release

Both apps always go out together, and nothing chooses one: Pro and Lite are the same app with ads and tracking switched off.

If one app's upload fails, press "Re-run failed jobs". Only that upload runs again, against the .ipa already built and signed - build number included, since it is baked in at archive time - and record runs behind it once it lands.

Nothing has to be committed to cut a release, and a release leaves no commit behind either. Both halves of the version come from outside the tree:

where it comes from what is checked in
MARKETING_VERSION (CFBundleShortVersionString) the version input 0.0.0
CURRENT_PROJECT_VERSION (CFBundleVersion) one above the highest build either app has 1

The version in project.pbxproj is a placeholder that only local and CI builds ever see; nobody bumps it, because a commit on main is not a release. The version has to be above what is live in the store - App Store Connect is the only thing that knows what that is, and it rejects the upload otherwise.

The build number is resolved once and given to both apps, so one (version, build) pair names one commit in both listings. App Store Connect only requires it to increase, not to be contiguous, so whichever app was behind skips ahead.

.github/scripts/resolve-version.py decides which version a run builds and refuses runs that cannot name one; changelog-section.py refuses a version with no CHANGELOG.md section, before anything is built, since that section becomes the release body. Run either by hand to see what a dispatch would do.

The dry_run input builds, signs and archives both .ipas without uploading either - the only way to exercise the signing path without putting a build on TestFlight. It is also the only kind of run allowed to go without a version, and the only one that leaves neither tag nor draft.

It needs these repository secrets:

secret what it is
ASC_KEY_ID App Store Connect API key id
ASC_ISSUER_ID issuer id of that key
ASC_KEY_CONTENT the .p8 private key, base64 encoded
SIGNING_CERTIFICATE_P12 Apple Distribution certificate + key as a base64 encoded .p12
SIGNING_CERTIFICATE_PASSWORD password of that .p12

The certificate is imported into a temporary keychain that is discarded with the runner, and signing is manual: fastlane downloads the App Store provisioning profile for the bundle id, and both the archive and the export use that certificate and profile. Automatic signing would instead have Xcode mint distribution assets of its own, which only an Admin key may do - anything less fails the export with "Cloud signing permission error".

Downloading a profile is something any key may do; creating one wants an Admin key. So a lesser key works as long as both apps have an App Store profile already - the run says so in its first seconds otherwise, and either an Admin key or a profile made by hand in the developer portal gets past it. Profiles expire after a year, which is the other moment this matters.

The same lanes work locally once those variables are exported, and take the version and the dry run the same way the workflow hands them over:

ODR_VERSION=1.36 bundle exec fastlane deployPro
ODR_VERSION=1.36 bundle exec fastlane deployLite
ODR_DRY_RUN=true bundle exec fastlane deployPro   # build and sign only

deployPro is buildPro followed by uploadPro, which the workflow runs as separate jobs. uploadPro takes the .ipa already in build/ rather than making one, and resolveBuildNumber prints the number both apps would get.

Tags

Nothing is triggered by a tag, and no tag is pushed before a build: a version often takes more than one build to get through review, so a tag pushed up front names a commit that may never ship. That is what happened to 1.37. Tags are written afterwards instead, in two kinds:

tag who writes it what it means
build/<version>/<build> the workflow, once both apps are up this commit was uploaded as that build
<version> publishing the drafted release this is what shipped

One build tag, not one per app, since both share a build number. It is never moved: a rebuild gets the next number, so a version that takes three builds to clear review leaves three build tags. A half uploaded release gets none, and neither does a lane run locally.

The version tag is written neither by hand nor by the workflow. record drafts a GitHub release named <version> - the changelog section with the generated list of pull requests below it - pointing at the built commit. A draft creates no tag; publishing it does, at exactly that commit:

gh release edit 1.38 --draft=false

That step stays human because App Store Connect is the only thing that knows a build went live. A rebuild re-points the same draft rather than making a second one, and if Pro clears review while Lite does not, wait: the build tags already record what went out.

About

It's Android's first OpenOffice Document Reader... for iOS!

Topics

Resources

Stars

17 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages