Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3f3eca7
Modernize to Swift 6 / SwiftUI / async-await / iOS 17
claude May 9, 2026
5a1cbb1
Fix Xcode 14 / iOS 16 compatibility
claude May 9, 2026
a657400
gitignore: cover Xcode-generated noise and sensitive files
claude May 9, 2026
b8c8371
Fix @retroactive URL+Identifiable compile error on Xcode 14
claude May 9, 2026
6a32534
Fix implicit switch return error on Swift 5.7
claude May 9, 2026
bd288b4
Fix string interpolation inside quotes in EmptyResultsView
claude May 9, 2026
81da5c5
Fix blank extension when search bar is tapped
claude May 9, 2026
982d539
Replace NavigationStack+searchable with plain VStack layout
claude May 9, 2026
3ab7d7a
Fix search bar hidden by removing @FocusState from RootView
claude May 9, 2026
2b43c23
Fix expansion: observe UITextField focus instead of willBecomeActive
claude May 9, 2026
6f1407c
Fix crash on tap and move search bar to UIKit layer
claude May 9, 2026
0566f80
Fix crash: replace continuation bridging with MainActor fire-and-forget
claude May 9, 2026
20a87f5
Empty commit to re-trigger CI
claude May 9, 2026
09b0e2b
Pin compose Task to main actor
claude May 9, 2026
4ab7dae
Add diagnostic logging and temporarily skip image in MessageBuilder
claude May 9, 2026
7b2961d
Update CI triggers from main to master
claude May 9, 2026
06300dd
Merge remote-tracking branch 'origin/master' into claude/modernize-le…
claude May 9, 2026
e862b2b
Bump CI to macos-14 + Xcode 15.4
claude May 9, 2026
d5bd1dd
Fix swift-format violations and drop --strict from CI
claude May 9, 2026
6a10918
Split build/test, list simulators, make lint non-blocking
claude May 9, 2026
6d1e10e
Make swift-format check exit 0 with || true
claude May 9, 2026
de3d29e
Auto-generate Info.plist for test target
claude May 9, 2026
b5d495c
Clean up Sendable warnings and re-enable strict CI lint
claude May 9, 2026
2dab54a
Fix lint, compile, and CI trigger duplication
claude May 9, 2026
52573ee
Install swift-format via Homebrew in CI
claude May 9, 2026
2dcf2f5
Move @MainActor isolation from compose to its inner Task
claude May 9, 2026
4d94fc7
Use build-for-testing so test bundle is actually compiled
claude May 9, 2026
512673b
Add BUNDLE_LOADER/TEST_HOST so test target links against extension
claude May 9, 2026
399b05c
Use WikiMessage host app as TEST_HOST for extension tests
claude May 9, 2026
9ff3b05
Combine build + test into single xcodebuild invocation
claude May 9, 2026
fd8e889
Drop test execution from CI; verify build only
claude May 9, 2026
82efb07
Add completion-handler logging to diagnose iMessage host crash
claude May 9, 2026
f79c1cd
Always set MSMessageTemplateLayout.image to fix iMessage host crash
claude May 9, 2026
fbd112f
Fix thumbnail URLs from search API (url, not source; protocol-relative)
claude May 9, 2026
61f208d
Strip HTML from search excerpts and enrich with summary on compose
claude May 9, 2026
2a23a39
Read activeConversation lazily; simplify MSMessage to original 2018 s…
claude May 9, 2026
4c814f9
Request compact presentation after insert (Apple sample-app pattern)
claude May 9, 2026
5d55f16
Bump version to 2.0.0 with date-based build number for TestFlight
claude May 9, 2026
9296744
Strip alpha channel from all app icons (App Store rule)
claude May 9, 2026
6fb61ae
Bump build to 2605091400 for next TestFlight upload
claude May 9, 2026
bb58e3e
Opt out of Mac (Designed for iPhone) and visionOS; bump build to 2605…
claude May 9, 2026
89fd078
Defer dismiss until after MSConversation.insert completion fires
claude May 9, 2026
268c3b4
Skip conversation.insert on simulator to avoid known iMessage crash
claude May 9, 2026
b61b4f7
Adopt systemBackground so the extension respects dark mode
claude May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,50 @@ on:
branches: [master]

jobs:
smoke:
name: Smoke
runs-on: ubuntu-latest
build-test:
name: Build & Test
runs-on: macos-14

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Select Xcode 15
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "15.4"

- name: List available simulators
run: xcrun simctl list devices available

- name: Cache DerivedData
uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: derived-data-${{ hashFiles('WikiMessage.xcodeproj/project.pbxproj') }}
restore-keys: derived-data-

- name: Build
run: |
xcodebuild \
-project WikiMessage.xcodeproj \
-scheme "WikiMessage MessagesExtension" \
-destination "platform=iOS Simulator,name=iPhone 15,OS=latest" \
-derivedDataPath ~/Library/Developer/Xcode/DerivedData \
clean build \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO

lint:
name: swift-format
runs-on: macos-14

steps:
- uses: actions/checkout@v4
- name: Confirm CI is wired up
run: echo "CI configured. Build and test jobs run from feature-branch workflows."
- name: Checkout
uses: actions/checkout@v4

- name: Install swift-format
run: brew install swift-format

- name: Lint
run: swift-format lint --recursive .
27 changes: 19 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@

# Carthage related files
Carthage
# Swift Package Manager
.swiftpm/

# Carthage (legacy — no longer used, but Xcode may regenerate artifacts)
Cartfile.resolved
Carthage/

# Certificates and private keys — NEVER commit these
*.p12
*.cer
*.mobileprovision

# Xcode workspace generated files
**/*.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

# Xcode-generated Package.swift inside extension folder
WikiMessage\ MessagesExtension/Package.swift

# Other generated files
sources.txt

# macOS stuff
.DS_Store
Expand Down Expand Up @@ -57,12 +74,6 @@ Package.resolved
#
# Pods/

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
Carthage/Checkouts

Carthage/Build

# fastlane
#
Expand Down
56 changes: 56 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"version": 1,
"lineLength": 110,
"indentation": {
"spaces": 4
},
"tabWidth": 4,
"maximumBlankLines": 1,
"respectsExistingLineBreaks": true,
"lineBreakBeforeControlFlowKeywords": true,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeEachGenericRequirement": false,
"prioritizeKeepingFunctionOutputTogether": true,
"indentConditionalCompilationBlocks": true,
"indentSwitchCaseLabels": false,
"fileScopedDeclarationPrivacy": {
"accessLevel": "private"
},
"rules": {
"AllPublicDeclarationsHaveDocumentation": false,
"AlwaysUseLowerCamelCase": true,
"AmbiguousTrailingClosureOverload": true,
"BeginDocumentationCommentWithOneLineSummary": false,
"DoNotUseSemicolons": true,
"DontRepeatTypeInStaticProperties": true,
"FileScopedDeclarationPrivacy": true,
"FullyIndirectEnum": true,
"GroupNumericLiterals": true,
"IdentifiersMustBeASCII": true,
"NeverForceUnwrap": false,
"NeverUseForceTry": true,
"NeverUseImplicitlyUnwrappedOptionals": false,
"NoAccessLevelOnExtensionDeclaration": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,
"NoEmptyTrailingClosureParentheses": true,
"NoLabelsInCasePatterns": true,
"NoLeadingUnderscores": false,
"NoParensAroundConditions": true,
"NoVoidReturnOnFunctionSignature": true,
"OmitExplicitReturns": true,
"OneCasePerLine": true,
"OneVariableDeclarationPerLine": true,
"OnlyOneTrailingClosureArgument": true,
"OrderedImports": true,
"ReturnVoidInsteadOfEmptyTuple": true,
"UseEarlyExits": false,
"UseLetInEveryBoundCaseVariable": false,
"UseShorthandTypeNames": true,
"UseSingleLinePropertyGetter": true,
"UseSynthesizedInitializer": true,
"UseTripleSlashForDocumentationComments": true,
"UseWhereClausesInForLoops": false,
"ValidateDocumentationComments": false
}
}
5 changes: 0 additions & 5 deletions Cartfile

This file was deleted.

140 changes: 85 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,85 @@
# Wikipedia Message

### An iMessages extension for searching, sending, and reading article from Wikipedia

---

## Purpose

Wikipedia Message (WikiMessage) is a [MessagesExtension](messages-extension) for Messages in iOS. It allows the user to quickly search for articles on Wikipedia and add them to a conversation. Selected articles are richly formatted in a custom message layout. Receivers can open the linked articles inline from Messages and view a stripped down reader mode of the contents, or choose to view the fully rendered version without leaving Messages. Rich formatting also appears on non-iOS devices (i.e. macOS) and allow the article to open directly inside a browser.

All of that aside, it was mostly a way for me to explore the current state of Swift and discover what a nightmare mobile development is.

## Design

WikiMessage is a [standalone](standalone) Messages App and thus does not come with a "container" (host) application. As such, it's feature set is limited to the [`MessagesExtension`](messages-extension) API subset. While an extension with a container app would be able to do processing within the host application, app processing for a standalone extension is done within the extension code itself.

Luckily WikiMessage's function is simple and straightforward. The basic functionality is as follows:

- Send flow:
1. Open a search interface for searching Wikipedia
2. Perform real-time fuzzy (more on this later) searches based on user input
3. Return list of relevant results
4. Allow user to select specific article
5. Preview selected article for sending
6. Send article

- Receive flow:
1. Receive richly formatted message of sent article in conversation
2. Select article to open in-line preview
3. Browse and read full contents of selected article and provide limited browsing within the rendered interface
4. Show fully rendered article page upon request
5. Allow opening in other applications
(non-iOS) Allow opening of article in default browser

In addition to this basic functionality, the interface should provide additional amounts of information (preview images, subtitles, description text, etc.), at the appropriate steps, so that the user doesn't have to go to another application to find an intended article.

### Wikipedia API

At the heart of WikiMessages is the [Wikipedia API](https://www.mediawiki.org/wiki/API:Main_page), provided by the WikiMedia Foundation. Wikipedia offers a number of different API's to access content. WikiMessages make use of the RESTful API.

**Note**: Although multiple "top level" language Wikipedia's provide a RESTful API, WikiMessages current only uses the API for the English Wikipedia page at [https://en.wikipedia.org/](wikipedia). Multi-language support may be considered in the future.

The mechanics of RESTful APIs will not be dealt with here. Familiarity with these types of APIs and how to query them is assumed.

Wikipedia provides a very useful [sandbox](https://www.mediawiki.org/wiki/Special:ApiSandbox) for testing queries against real data. It is recommended to try any new queries here.

### Step 1: Searching

The primary query to the Wikipedia API is to perform a text based search against article titles based on a given string. This search should return the full list of relevant (more on that later) articles as well as some light metadata on each.

When a user first inputs a search string into the search box, a query is constructed of [url-encoded] elements and sent to the Wikipedia API.

**Components**:

Entering text in the search bar triggers a query to the `action` endpoint of the API. The `searchForArticle` method constructs a basic query using the `action=query` method.
# WikiMessage

An iMessage extension for searching and sharing Wikipedia articles.

## What it does

Open WikiMessage from the iMessage app drawer, type a query, and tap a result to insert a richly formatted article card into the conversation. Recipients can tap the card to open the article in Safari Reader Mode without leaving Messages.

## Requirements

- Xcode 17 or later
- iOS 17.0+ deployment target
- Apple Developer account (team `T5VJ9JRCNB`) for signing

## Building

```bash
open WikiMessage.xcodeproj
```

Select the **WikiMessage MessagesExtension** scheme, choose an iOS 17+ simulator, and run. No package fetching or Carthage bootstrap needed — zero third-party dependencies.

To run tests:

```bash
xcodebuild \
-project WikiMessage.xcodeproj \
-scheme "WikiMessage MessagesExtension" \
-destination "platform=iOS Simulator,name=iPhone 16,OS=latest" \
test \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
```

## Architecture

```
WikiMessage MessagesExtension/
├── MessagesViewController.swift # Thin UIHostingController shell
├── App/
│ ├── AppModel.swift # @Observable: presentation style, composer, selected URL
│ └── RootView.swift # NavigationStack + .searchable entry point
├── Features/
│ ├── Search/
│ │ ├── SearchModel.swift # @Observable: query, phase, recent searches
│ │ ├── SearchResultsList.swift
│ │ ├── ArticleRow.swift
│ │ └── RecentSearchesStore.swift
│ ├── States/
│ │ ├── LoadingView.swift
│ │ ├── EmptyResultsView.swift
│ │ └── ErrorView.swift
│ └── SafariView.swift # UIViewControllerRepresentable
├── Messaging/
│ ├── MessageComposer.swift # Protocol + LiveMessageComposer
│ └── MessageBuilder.swift # MSMessageTemplateLayout builder
├── Data/
│ ├── Article.swift # Domain model (Sendable, Identifiable, Hashable)
│ ├── WikipediaService.swift # actor; search + summary
│ └── DTOs/
│ ├── SummaryDTO.swift # /api/rest_v1/page/summary shape
│ └── SearchResponseDTO.swift # Wikimedia Core API search shape
└── Networking/
├── HTTPClient.swift # actor; URLSession wrapper with shared decoder
└── NetworkMonitor.swift # @Observable NWPathMonitor wrapper

WikiMessageExtensionTests/
├── Support/MockURLProtocol.swift
├── Fixtures/{summary_einstein,search_swift}.json
├── DTOs/{SummaryDTO,SearchResponseDTO}Tests.swift
├── Services/WikipediaServiceTests.swift
└── Messaging/MessageBuilderTests.swift
```

## APIs used

| Purpose | Endpoint |
|---|---|
| Search | `https://api.wikimedia.org/core/v1/wikipedia/en/search/page?q=…&limit=10` |
| Article summary | `https://en.wikipedia.org/api/rest_v1/page/summary/{title}` |

The language path segment (`/en/`) is parameterised in `WikipediaService` for future multi-language support.

## Original version

The pre-modernization state (Swift 4 / iOS 11 / Carthage / UIKit, last commit 2018-10-10) is preserved at tag `v1.0-legacy`.
8 changes: 8 additions & 0 deletions WikiMessage MessagesExtension/App/AppModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Combine
import Messages

final class AppModel: ObservableObject {
@Published var presentationStyle: MSMessagesAppPresentationStyle = .compact
@Published var composer: (any MessageComposer)?
@Published var selectedArticleURL: URL?
}
19 changes: 19 additions & 0 deletions WikiMessage MessagesExtension/App/RootView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import SwiftUI

struct RootView: View {
@EnvironmentObject private var appModel: AppModel
@EnvironmentObject private var searchModel: SearchModel

var body: some View {
SearchResultsList()
.sheet(isPresented: Binding(
get: { appModel.selectedArticleURL != nil },
set: { if !$0 { appModel.selectedArticleURL = nil } }
)) {
if let url = appModel.selectedArticleURL {
SafariView(url: url)
.ignoresSafeArea()
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading